Skip to content

Commit

Permalink
Merge pull request #694 from smapiot/develop
Browse files Browse the repository at this point in the history
Release 1.5.5
  • Loading branch information
FlorianRappl committed May 10, 2024
2 parents 275738f + 1d44d2f commit 8e2ed6d
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 9 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Piral Changelog

## 1.5.5 (tbd)

- Fixed `piral-ng/extend-webpack` for MJS files using not fully specified references
- Added `piral-ng/standalone` to support pure modern Angular standalone (#690)

## 1.5.4 (April 23, 2024)

- Fixed `pilet build` with `--type standalone` to end up at feed selection
Expand Down
58 changes: 57 additions & 1 deletion src/converters/piral-ng/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ which defines the selector (`angular-page`) matching the specified selector in t

### Standalone Components

The `piral-ng` plugin also supports Angular standalone components as rendering source.
The `piral-ng` plugin also supports Angular standalone components as rendering source. For this we have two modes:

#### Legacy Standalone Mode

This mode works with `piral-ng` itself, i.e., integrated in the app shell. It also works in a mix with modules.

Standalone components can also be used with lazy loading.

Expand All @@ -121,6 +125,58 @@ export function setup(piral: PiletApi) {
}
```

#### Isolated Standalone Mode

This mode works only with `piral-ng/standalone`, which has to be used in a pilet directly (as a replacement for `piral-ng/convert`). It does not mix with modules - as components need to be proper standalone entry points.

```ts
import "core-js/proposals/reflect-metadata";
import "zone.js";
import { createConverter } from 'piral-ng/standalone';
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { PiletApi } from '<name-of-piral-instance>';
import { AngularPage } from './AngularPage';

const appConfig: ApplicationConfig = {
providers: [
provideRouter([
{
path: "sample",
component: AngularPage,
},
]),
],
};

export function setup(piral: PiletApi) {
const fromNg = createConverter(appConfig);

piral.registerPage('/sample', fromNg(AngularPage));
}
```

Lazy loading is still possible, e.g., over the `loadComponent` from the routes definition or by supplying a callback to the `fromNg` function, e.g.:

```ts
import "core-js/proposals/reflect-metadata";
import "zone.js";
import { createConverter } from "piral-ng/standalone";
import { ApplicationConfig } from "@angular/core";
import type { PiletApi } from "sample-piral";

const appConfig: ApplicationConfig = {
providers: [],
};

export function setup(app: PiletApi) {
const fromNg = createConverter(appConfig);

app.registerExtension('foo', fromNg(() => import('./app/extension.component')));
}

```

### Angular Options

You can optionally provide Options to `defineNgModule`, which are identical to those given to `bootstrapModule` during the Angular boot process. See https://angular.io/api/core/PlatformRef#bootstrapModule for possible values.
Expand Down
5 changes: 5 additions & 0 deletions src/converters/piral-ng/extend-webpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ module.exports =
test: /\.[jt]sx?$/,
loader: ngtoolsLoader,
},
{
test: /\.mjs$/,
loader: ngtoolsLoader,
resolve: { fullySpecified: false },
},
{
test: /\.component.html$/i,
use: [toStringLoader, htmlLoaderNoModule],
Expand Down
3 changes: 3 additions & 0 deletions src/converters/piral-ng/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"./extend-webpack": {
"require": "./extend-webpack.js"
},
"./standalone": {
"import": "./esm/standalone.js"
},
"./esm/*": {
"import": "./esm/*"
},
Expand Down
85 changes: 85 additions & 0 deletions src/converters/piral-ng/src/CoreRoutingService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Subscription } from 'rxjs';
import type { ComponentContext, Disposable } from 'piral-core';
import { Inject, Injectable, NgZone, OnDestroy, Optional } from '@angular/core';
import { NavigationEnd, NavigationError, NavigationStart, Router, Scroll } from '@angular/router';
import { BrowserPlatformLocation as ɵBrowserPlatformLocation } from '@angular/common';

const noop = function () {};

// deactivates the usual platform behavior; all these operations are performed via the RoutingService
// to avoid any conflict, e.g., double-booking URL changes in React and Angular
ɵBrowserPlatformLocation.prototype.pushState = noop;
ɵBrowserPlatformLocation.prototype.replaceState = noop;
ɵBrowserPlatformLocation.prototype.forward = noop;
ɵBrowserPlatformLocation.prototype.back = noop;
ɵBrowserPlatformLocation.prototype.historyGo = noop;

function normalize(url: string) {
const search = url.indexOf('?');
const hash = url.indexOf('#');

if (search !== -1 || hash !== -1) {
if (search === -1) {
return url.substring(0, hash);
} else if (hash === -1) {
return url.substring(0, search);
} else {
return url.substring(0, Math.min(search, hash));
}
}

return url;
}

@Injectable()
export class CoreRoutingService implements OnDestroy {
private dispose: Disposable | undefined;
private subscription: Subscription | undefined;

constructor(
@Inject('Context') public context: ComponentContext,
@Optional() private router: Router,
@Optional() private zone: NgZone,
) {
if (this.router) {
this.router.errorHandler = (error: Error) => {
// Match in development and production
if (error.message.match('Cannot match any routes') || error.message.match('NG04002')) {
// ignore this special error
return undefined;
}
throw error;
};

const nav = this.context.navigation;

const queueNavigation = (url: string) => {
window.requestAnimationFrame(() => nav.push(url));
};

this.dispose = nav.listen(({ location }) => {
const path = location.pathname;
const url = `${path}${location.search}${location.hash}`;
this.zone.run(() => this.router.navigateByUrl(url));
});

this.subscription = this.router.events.subscribe(
(e: NavigationError | NavigationStart | NavigationEnd | Scroll) => {
if (e.type === 1) {
const routerUrl = normalize(e.urlAfterRedirects);
const locationUrl = nav.url;

if (routerUrl !== locationUrl) {
queueNavigation(routerUrl);
}
}
},
);
}
}

ngOnDestroy() {
this.dispose?.();
this.subscription?.unsubscribe();
}
}
102 changes: 102 additions & 0 deletions src/converters/piral-ng/src/standalone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { APP_BASE_HREF } from '@angular/common';
import { createApplication } from '@angular/platform-browser';
import {
ApplicationConfig,
ApplicationRef,
ComponentRef,
Type,
ɵresetCompiledComponents as reset,
} from '@angular/core';
import type { BaseComponentProps, HtmlComponent } from 'piral-core';
import { CoreRoutingService } from './CoreRoutingService';

function isLazyLoader<TProps>(thing: NgStandaloneComponent<TProps>): thing is NgStandaloneComponentLoader<TProps> {
return typeof thing === 'function' && thing.hasOwnProperty('prototype') && thing.hasOwnProperty('arguments');
}

export interface DefaultExport<T> {
default: T;
}

export type NgStandaloneComponentLoader<TProps> = () => Promise<DefaultExport<Type<TProps>>>;

export type NgStandaloneComponent<TProps> = Type<TProps> | NgStandaloneComponentLoader<TProps>;

export interface NgStandaloneConverter {
<TProps extends BaseComponentProps>(component: NgStandaloneComponent<TProps>): HtmlComponent<TProps>;
}

export function createConverter(options: ApplicationConfig): NgStandaloneConverter {
const update = (ref: ComponentRef<any>, props: any) => {
if (ref) {
const ct = ref.componentType as any;

if (ct?.ɵcmp?.inputs?.Props) {
ref.setInput('Props', props);
}
}
};

let app: undefined | Promise<ApplicationRef> = undefined;

return (component) => ({
type: 'html',
component: {
mount(element, props, ctx, locals) {
if (!app) {
const { piral } = props;

app = createApplication({
...options,
providers: [
...options.providers,
CoreRoutingService,
{ provide: APP_BASE_HREF, useValue: ctx.publicPath },
{ provide: 'Context', useValue: ctx },
{ provide: 'piral', useValue: piral },
],
});

piral.on('unload-pilet', (ev) => {
if (ev.name === piral.meta.name && typeof reset === 'function') {
// pretty much a cleanup step for Angular.
reset();
}
});
}

locals.active = true;

app
.then((appRef) => {
if (isLazyLoader(component)) {
const lazyComponent = component();
return lazyComponent.then((componentExports) => [appRef, componentExports.default] as const);
}

return [appRef, component] as const;
})
.then(([appRef, component]) => {
if (locals.active) {
const ref = appRef.bootstrap(component, element);

// Start the routing service.
appRef.injector.get(CoreRoutingService);

update(ref, props);
locals.component = ref;
}
});
},
update(_1, props, _2, locals) {
update(locals.component, props);
},
unmount(element, locals) {
locals.active = false;
locals.component?.destroy();
locals.component = undefined;
element.remove();
},
},
});
}
2 changes: 1 addition & 1 deletion src/tooling/piral-cli/src/apps/build-piral.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const tsConfigJson = `
"outDir": "./lib",
"skipLibCheck": true,
"lib": ["dom", "es2018"],
"moduleResolution": "node",
"moduleResolution": "Bundler",
"module": "esnext",
"jsx": "react",
"importHelpers": true
Expand Down
2 changes: 1 addition & 1 deletion src/tooling/piral-cli/src/common/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ export async function getSourceFiles(entry: string) {
checkJs: false,
jsx: JsxEmit.React,
module: ModuleKind.ESNext,
moduleResolution: ModuleResolutionKind.NodeJs,
moduleResolution: ModuleResolutionKind.Bundler,
target: ScriptTarget.ESNext,
},
}).outputText;
Expand Down
37 changes: 31 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5393,9 +5393,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==

ejs@^3.1.7:
version "3.1.9"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==
version "3.1.10"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
dependencies:
jake "^10.8.5"

Expand Down Expand Up @@ -11377,7 +11377,16 @@ stop-iteration-iterator@^1.0.0:
dependencies:
internal-slot "^1.0.4"

"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand Down Expand Up @@ -11422,7 +11431,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
Expand All @@ -11443,6 +11452,13 @@ strip-ansi@^5.1.0:
dependencies:
ansi-regex "^4.1.0"

strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"

strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
Expand Down Expand Up @@ -12546,7 +12562,7 @@ workerpool@^3.1.1:
object-assign "4.1.1"
rsvp "^4.8.4"

"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
Expand All @@ -12564,6 +12580,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
Expand Down

0 comments on commit 8e2ed6d

Please sign in to comment.