@gjsify/rolldown-plugin-gjsify 0.64.12 → 0.64.13-next.283
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app/nativescript.js +4 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +2 -0
- package/lib/plugins/gi-optional.js +1 -1
- package/lib/plugins/nativescript-gi-selection.d.ts +11 -0
- package/lib/plugins/nativescript-gi-selection.js +74 -0
- package/lib/plugins/nativescript-locale.d.ts +20 -0
- package/lib/plugins/nativescript-locale.js +56 -0
- package/lib/types/plugin-options.d.ts +7 -0
- package/lib/utils/gi-namespaces.d.ts +23 -0
- package/lib/utils/gi-namespaces.js +275 -0
- package/lib/utils/gi-selection.d.ts +31 -0
- package/lib/utils/gi-selection.js +94 -0
- package/lib/utils/index.d.ts +6 -0
- package/lib/utils/index.js +3 -0
- package/lib/utils/module-ast.d.ts +62 -0
- package/lib/utils/module-ast.js +103 -0
- package/package.json +13 -11
package/lib/app/nativescript.js
CHANGED
|
@@ -44,6 +44,7 @@ import { ALIASES_NODE_FOR_NATIVESCRIPT, GI_RENDERERS, getDerivedAliasesSync } fr
|
|
|
44
44
|
import { globToEntryPoints } from '../utils/entry-points.js';
|
|
45
45
|
import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
|
|
46
46
|
import { giRendererPlugin } from '../plugins/gi-renderer.js';
|
|
47
|
+
import { nativescriptLocalePlugin } from '../plugins/nativescript-locale.js';
|
|
47
48
|
import { blueprintTemplateExit } from '../utils/blueprint-template-exit.js';
|
|
48
49
|
import { platformResolvePlugin, nativescriptSuffixChain, detectNativescriptPlatform, nativescriptPlatformDefines, } from '../plugins/platform-resolve.js';
|
|
49
50
|
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
@@ -190,6 +191,9 @@ export const setupForNativescript = async (input) => {
|
|
|
190
191
|
// holds the author's intent; the guard and the absence are untouched.
|
|
191
192
|
gschemaPlugin({ mode: 'emit', register: GSCHEMA_REGISTER.nativescript }),
|
|
192
193
|
gresourcePlugin({ compressed: 'strip' }),
|
|
194
|
+
...(input.pluginOptions.nativescriptLocale
|
|
195
|
+
? [nativescriptLocalePlugin(input.pluginOptions.nativescriptLocale)]
|
|
196
|
+
: []),
|
|
193
197
|
implicitGlobalAssignPlugin(),
|
|
194
198
|
// NO cssAsStringPlugin — NS ships its own CSS pipeline via
|
|
195
199
|
// @nativescript/core; .css imports are handled by the consuming
|
package/lib/index.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export { aliasPlugin } from './plugins/alias.js';
|
|
|
26
26
|
export type { AliasPluginOptions } from './plugins/alias.js';
|
|
27
27
|
export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
|
|
28
28
|
export { giRendererPlugin } from './plugins/gi-renderer.js';
|
|
29
|
+
export { nativescriptLocalePlugin, collectLocaleAssets, NATIVESCRIPT_LOCALE_DIR, NATIVESCRIPT_LOCALE_PLUGIN, } from './plugins/nativescript-locale.js';
|
|
30
|
+
export type { NativescriptLocaleOptions, LocaleAsset } from './plugins/nativescript-locale.js';
|
|
31
|
+
export { nativescriptGiSelectionPlugin, NATIVESCRIPT_GI_SELECTION_PLUGIN, } from './plugins/nativescript-gi-selection.js';
|
|
32
|
+
export type { NativescriptGiSelectionOptions } from './plugins/nativescript-gi-selection.js';
|
|
29
33
|
export { externalsPlugin } from './plugins/externals.js';
|
|
30
34
|
export type { ExternalsPredicate, ExternalsPluginOptions } from './plugins/externals.js';
|
|
31
35
|
export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier, formatUnresolvedWorkspaceImport, buildReverseAliasIndex, UnresolvedWorkspaceImportError, } from './plugins/unresolved-workspace-import.js';
|
package/lib/index.js
CHANGED
|
@@ -17,6 +17,8 @@ export { shebangPlugin, GJS_SHEBANG, NODE_SHEBANG, expandEnvTemplate, resolveShe
|
|
|
17
17
|
export { aliasPlugin } from './plugins/alias.js';
|
|
18
18
|
export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
|
|
19
19
|
export { giRendererPlugin } from './plugins/gi-renderer.js';
|
|
20
|
+
export { nativescriptLocalePlugin, collectLocaleAssets, NATIVESCRIPT_LOCALE_DIR, NATIVESCRIPT_LOCALE_PLUGIN, } from './plugins/nativescript-locale.js';
|
|
21
|
+
export { nativescriptGiSelectionPlugin, NATIVESCRIPT_GI_SELECTION_PLUGIN, } from './plugins/nativescript-gi-selection.js';
|
|
20
22
|
export { externalsPlugin } from './plugins/externals.js';
|
|
21
23
|
export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier, formatUnresolvedWorkspaceImport, buildReverseAliasIndex, UnresolvedWorkspaceImportError, } from './plugins/unresolved-workspace-import.js';
|
|
22
24
|
export { napiNodeAddonPlugin, resolveAddonPath, enumerateAddonTargets, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, isGjsifyNativeBridge, detectNapiRsEntry, hostNapiRsTriple, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// The `optional` query flag is the declaration that the app has that decision to make.
|
|
8
8
|
// The import resolves to a virtual module that loads the SAME specifier without the
|
|
9
9
|
// flag inside a try/catch, so the emitted bundle still carries the verbatim
|
|
10
|
-
// `gi://Goa?version=1.0` that `
|
|
10
|
+
// `gi://Goa?version=1.0` that `utils/gi-namespaces.ts` and the CLI's `depends.ts` read, and the
|
|
11
11
|
// namespace is `undefined` when it is absent.
|
|
12
12
|
//
|
|
13
13
|
// BOTH app targets, and the second one was the trap. `--app node` already loads a
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown';
|
|
2
|
+
export declare const NATIVESCRIPT_GI_SELECTION_PLUGIN = "gjsify-nativescript-gi-selection";
|
|
3
|
+
export interface NativescriptGiSelectionOptions {
|
|
4
|
+
/** `giNamespaces` as the app declares it, `Ns` or `Ns-Version`. */
|
|
5
|
+
declared: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Refuse a NativeScript bundle that imports a GI namespace the APK was not told to
|
|
9
|
+
* carry, and warn about one it was told to carry and does not appear to import.
|
|
10
|
+
*/
|
|
11
|
+
export declare function nativescriptGiSelectionPlugin(options: NativescriptGiSelectionOptions): Plugin;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The ADR 0117 § 2 gate on the VITE path: every GI namespace the emitted bundle loads
|
|
3
|
+
// has to be one the APK was told to carry.
|
|
4
|
+
//
|
|
5
|
+
// WHY A SECOND PLACE AT ALL. `gjsify build --app nativescript` already checks this in
|
|
6
|
+
// `actions/build.ts`, but a NativeScript app is not built that way: the NS CLI drives
|
|
7
|
+
// Vite, so `defineNativescriptConfig` is the only thing in the loop. Without this the
|
|
8
|
+
// key is accepted by the config loader and acted on by nothing — the exact shape this
|
|
9
|
+
// repo keeps paying for, and on Android it is not advice: a `.deb` that under-declares
|
|
10
|
+
// its typelibs can still be fixed by installing a package, while an APK carries its
|
|
11
|
+
// whole GI stack or does without it.
|
|
12
|
+
//
|
|
13
|
+
// SAME ORACLE, SAME PLANNER, deliberately: {@link planGiSelection} over
|
|
14
|
+
// {@link scanGiNamespaces} of the emitted chunks, so the two paths cannot disagree
|
|
15
|
+
// about what a given config means. `generateBundle` rather than a `resolveId`
|
|
16
|
+
// observer, and that was MEASURED, not preferred:
|
|
17
|
+
//
|
|
18
|
+
// with `giRenderer: true` (what every NS app here uses) the renderer plugin answers
|
|
19
|
+
// `gi://Gtk` and `gi://Adw` with `@gjsify/adwaita-nativescript`'s namespaces, so those
|
|
20
|
+
// specifiers are in the module graph of a build that needs NO Gtk typelib at all.
|
|
21
|
+
// Feeding them to the planner would demand `giNamespaces: ["Gtk", "Adw"]`, which the
|
|
22
|
+
// pinned sysroot cannot satisfy (no `cairo-1.0` / `freetype2-2.0` / `fontconfig-2.0`
|
|
23
|
+
// typelib), i.e. it would refuse a build that works today.
|
|
24
|
+
//
|
|
25
|
+
// The emitted file is what gets installed, and it carries only what survived the
|
|
26
|
+
// renderer. That is the honest input.
|
|
27
|
+
//
|
|
28
|
+
// The scan is a LOWER BOUND, so only one direction is fatal — the asymmetry
|
|
29
|
+
// `gi-selection.ts` carries in full.
|
|
30
|
+
import { scanGiNamespaces } from '../utils/gi-namespaces.js';
|
|
31
|
+
import { assertGiNamespaceEntries, planGiSelection } from '../utils/gi-selection.js';
|
|
32
|
+
export const NATIVESCRIPT_GI_SELECTION_PLUGIN = 'gjsify-nativescript-gi-selection';
|
|
33
|
+
/**
|
|
34
|
+
* Refuse a NativeScript bundle that imports a GI namespace the APK was not told to
|
|
35
|
+
* carry, and warn about one it was told to carry and does not appear to import.
|
|
36
|
+
*/
|
|
37
|
+
export function nativescriptGiSelectionPlugin(options) {
|
|
38
|
+
// Checked when the plugin is composed, not when the bundle is written: a typo
|
|
39
|
+
// should fail before a full NativeScript build, and this is the Vite path's only
|
|
40
|
+
// reader of the key.
|
|
41
|
+
assertGiNamespaceEntries(options.declared, `${NATIVESCRIPT_GI_SELECTION_PLUGIN}: giNamespaces`);
|
|
42
|
+
return {
|
|
43
|
+
name: NATIVESCRIPT_GI_SELECTION_PLUGIN,
|
|
44
|
+
generateBundle(_outputOptions, bundle) {
|
|
45
|
+
// Namespace → the chunk it was first read from, so a refusal names a file
|
|
46
|
+
// somebody can open. `planGiSelection` is pure and knows neither.
|
|
47
|
+
const origin = new Map();
|
|
48
|
+
for (const [fileName, item] of Object.entries(bundle)) {
|
|
49
|
+
if (item.type !== 'chunk')
|
|
50
|
+
continue;
|
|
51
|
+
const code = item.code;
|
|
52
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
53
|
+
continue;
|
|
54
|
+
for (const namespace of scanGiNamespaces(code)) {
|
|
55
|
+
if (!origin.has(namespace))
|
|
56
|
+
origin.set(namespace, fileName);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
let selection;
|
|
60
|
+
try {
|
|
61
|
+
selection = planGiSelection({ declared: [...options.declared], scanned: [...origin.keys()] });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const files = [...new Set(origin.values())].sort().join(', ');
|
|
65
|
+
throw new Error(`${NATIVESCRIPT_GI_SELECTION_PLUGIN}: ${error.message} (read from ${files})`);
|
|
66
|
+
}
|
|
67
|
+
for (const namespace of selection.unscanned) {
|
|
68
|
+
this.warn(`${NATIVESCRIPT_GI_SELECTION_PLUGIN}: giNamespaces declares ${namespace}, which the bundle ` +
|
|
69
|
+
'does not appear to import. The APK carries it anyway — drop it if nothing needs it, ' +
|
|
70
|
+
'since every selected namespace costs APK size.');
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown';
|
|
2
|
+
export declare const NATIVESCRIPT_LOCALE_PLUGIN = "gjsify-nativescript-locale";
|
|
3
|
+
/** Directory below the app root (`<filesDir>/app`) that holds the catalogs. */
|
|
4
|
+
export declare const NATIVESCRIPT_LOCALE_DIR = "locale";
|
|
5
|
+
export interface NativescriptLocaleOptions {
|
|
6
|
+
/** Directory with the `<lang>.po` catalogs. */
|
|
7
|
+
poDirectory: string;
|
|
8
|
+
/** gettext domain — the `.mo` file name, and what the app passes to `bindtextdomain`. */
|
|
9
|
+
domain: string;
|
|
10
|
+
/** Output names for catalogs the built-in table does not know, as in `gettextPlugin`. */
|
|
11
|
+
localeNames?: Record<string, readonly string[]>;
|
|
12
|
+
}
|
|
13
|
+
export interface LocaleAsset {
|
|
14
|
+
fileName: string;
|
|
15
|
+
source: Uint8Array;
|
|
16
|
+
}
|
|
17
|
+
/** `msgfmt` with an argv array (no shell); `-o -` streams the `.mo` to stdout. */
|
|
18
|
+
export declare function msgfmtToBuffer(poFile: string): Promise<Uint8Array>;
|
|
19
|
+
export declare function collectLocaleAssets(options: NativescriptLocaleOptions, compile?: (poFile: string) => Promise<Uint8Array>): Promise<LocaleAsset[]>;
|
|
20
|
+
export declare function nativescriptLocalePlugin(options: NativescriptLocaleOptions): Plugin;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Compile an app's `po/` into `locale/<lang>/LC_MESSAGES/<domain>.mo` ASSETS of the bundle.
|
|
2
|
+
//
|
|
3
|
+
// On Android the GLib gettext (gi-runtime-android's libintl reader) opens the catalog from the
|
|
4
|
+
// real filesystem, and NativeScript extracts `assets/app/**` to `<filesDir>/app/**`. So the `.mo`
|
|
5
|
+
// tree has to ride along in the staging dir the NS build copies — emitted as assets rather than
|
|
6
|
+
// written beside the output, because Vite's `emptyOutDir` wipes anything written before the
|
|
7
|
+
// bundle. The runtime resolves the same directory with `localeDir` (`@gjsify/adwaita-app/locale`).
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
import { findAvailableLanguages, planCatalogNames } from '@gjsify/vite-plugin-gettext';
|
|
13
|
+
const run = promisify(execFile);
|
|
14
|
+
export const NATIVESCRIPT_LOCALE_PLUGIN = 'gjsify-nativescript-locale';
|
|
15
|
+
/** Directory below the app root (`<filesDir>/app`) that holds the catalogs. */
|
|
16
|
+
export const NATIVESCRIPT_LOCALE_DIR = 'locale';
|
|
17
|
+
/** `msgfmt` with an argv array (no shell); `-o -` streams the `.mo` to stdout. */
|
|
18
|
+
export async function msgfmtToBuffer(poFile) {
|
|
19
|
+
const { stdout } = await run('msgfmt', ['--output-file=-', poFile], {
|
|
20
|
+
encoding: 'buffer',
|
|
21
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
22
|
+
});
|
|
23
|
+
return stdout;
|
|
24
|
+
}
|
|
25
|
+
export async function collectLocaleAssets(options, compile = msgfmtToBuffer) {
|
|
26
|
+
if (!existsSync(options.poDirectory)) {
|
|
27
|
+
throw new Error(`${NATIVESCRIPT_LOCALE_PLUGIN}: PO directory ${options.poDirectory} does not exist`);
|
|
28
|
+
}
|
|
29
|
+
const languages = await findAvailableLanguages(options.poDirectory, NATIVESCRIPT_LOCALE_PLUGIN, false);
|
|
30
|
+
const plans = planCatalogNames(languages, {
|
|
31
|
+
pluginName: NATIVESCRIPT_LOCALE_PLUGIN,
|
|
32
|
+
namespace: 'posix',
|
|
33
|
+
localeNames: options.localeNames,
|
|
34
|
+
});
|
|
35
|
+
const assets = [];
|
|
36
|
+
for (const plan of plans) {
|
|
37
|
+
const source = await compile(join(options.poDirectory, `${plan.catalog}.po`));
|
|
38
|
+
for (const dir of plan.posix) {
|
|
39
|
+
assets.push({
|
|
40
|
+
fileName: `${NATIVESCRIPT_LOCALE_DIR}/${dir}/LC_MESSAGES/${options.domain}.mo`,
|
|
41
|
+
source,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return assets;
|
|
46
|
+
}
|
|
47
|
+
export function nativescriptLocalePlugin(options) {
|
|
48
|
+
return {
|
|
49
|
+
name: NATIVESCRIPT_LOCALE_PLUGIN,
|
|
50
|
+
async generateBundle() {
|
|
51
|
+
for (const asset of await collectLocaleAssets(options)) {
|
|
52
|
+
this.emitFile({ type: 'asset', fileName: asset.fileName, source: asset.source });
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NativescriptLocaleOptions } from '../plugins/nativescript-locale.js';
|
|
1
2
|
import type { App } from './app.js';
|
|
2
3
|
import type { SourceDialect } from './source-dialect.js';
|
|
3
4
|
/** CSS handling forwarded to Rolldown / Lightning CSS. */
|
|
@@ -105,6 +106,12 @@ export interface PluginOptions {
|
|
|
105
106
|
* every unanswerable namespace from a silent stub into a hard build failure.
|
|
106
107
|
*/
|
|
107
108
|
giRenderer?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* `--app nativescript` only: compile this PO directory to
|
|
111
|
+
* `locale/<lang>/LC_MESSAGES/<domain>.mo` assets of the bundle, so the Android libintl
|
|
112
|
+
* reader finds them under `<filesDir>/app/locale`. See `plugins/nativescript-locale.ts`.
|
|
113
|
+
*/
|
|
114
|
+
nativescriptLocale?: NativescriptLocaleOptions;
|
|
108
115
|
/**
|
|
109
116
|
* Where to look for a `@gjsify/*` the PROJECT cannot resolve, when the input is
|
|
110
117
|
* TOOLCHAIN rather than user code: a file path inside the running CLI's own
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a bundle loads, split by whether the host must provide it.
|
|
3
|
+
*
|
|
4
|
+
* `namespaces` is the union — the same answer {@link scanGiNamespaces} has always
|
|
5
|
+
* given — and `optional` is the subset the artifact itself marks optional. A namespace
|
|
6
|
+
* in both is still a hard dependency as far as anything else is concerned: the
|
|
7
|
+
* importer asked for it both ways, and only one of those edges degrades.
|
|
8
|
+
*/
|
|
9
|
+
export interface GiRequirements {
|
|
10
|
+
/** Every GI namespace the bundle loads, `Ns-Version` where a specifier pins one. */
|
|
11
|
+
namespaces: string[];
|
|
12
|
+
/** The subset carrying the bundler's optional marker — absent is not fatal. */
|
|
13
|
+
optional: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Extract the GI namespaces a bundle imports, as `Ns-Version` when the
|
|
17
|
+
* specifier pins one and bare `Ns` when it does not.
|
|
18
|
+
*/
|
|
19
|
+
export declare function scanGiNamespaces(source: string): string[];
|
|
20
|
+
/** {@link scanGiNamespaces}, plus which of them the artifact marks optional (ADR 0087). */
|
|
21
|
+
export declare function scanGiRequirements(source: string): GiRequirements;
|
|
22
|
+
/** `gi://Gtk?version=4.0` → `Gtk-4.0`; `gi://Gtk` → `Gtk`; anything else → null. */
|
|
23
|
+
export declare function giSpecifierKey(specifier: string): string | null;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// Which GI namespaces does this bundle actually load?
|
|
2
|
+
//
|
|
3
|
+
// Read off the EMITTED bundle, because that is the file that gets installed —
|
|
4
|
+
// and the emitted form is not one form but two, which is the correction this
|
|
5
|
+
// header carries. Under `--app gjs`, `gi://` is a real module protocol, so the
|
|
6
|
+
// bundler keeps those specifiers in the output verbatim
|
|
7
|
+
// (`rolldown-plugin-gjsify`'s externals plugin) and the artifact carries its own
|
|
8
|
+
// dependency list. Under `--app node` there is no such protocol:
|
|
9
|
+
// `gjsGiNodePlugin` rewrites every `gi://Ns?version=X` into a shim whose body is
|
|
10
|
+
// `require('@gjsify/node-gi/gi').requireGi("Ns", "X")`, so the specifier this
|
|
11
|
+
// scanner was written to find is gone from the file by construction.
|
|
12
|
+
//
|
|
13
|
+
// WHAT THAT COST, and why reading one form is not "mostly right": these
|
|
14
|
+
// namespaces are what `depends.ts` maps to `gir1.2-gtk-4.0` and friends
|
|
15
|
+
// (ADR 0024 § 6). A `--app node` project therefore derived an EMPTY typelib
|
|
16
|
+
// dependency set — the package installed cleanly and died at its first GI call,
|
|
17
|
+
// which is the same failure the bare-side-effect-import fix below was written to
|
|
18
|
+
// prevent, arriving again through the other build target. Nothing caught it
|
|
19
|
+
// because every ship fixture's "node bundle" was hand-written with `gi://`
|
|
20
|
+
// imports in it: a shape `gjsify build --app node` cannot emit and node cannot
|
|
21
|
+
// run.
|
|
22
|
+
//
|
|
23
|
+
// PARSED, not pattern-matched, and the first version of this file is why. A
|
|
24
|
+
// regex over the bundle text got it wrong in both directions at once:
|
|
25
|
+
//
|
|
26
|
+
// * it missed `import "gi://Soup?version=3.0"` — the bare side-effect form,
|
|
27
|
+
// which is exactly what `@gjsify/fetch` puts at the top of every bundle
|
|
28
|
+
// that pulls it. The package would have shipped without libsoup, installed
|
|
29
|
+
// cleanly, and died at the first request.
|
|
30
|
+
// * it matched `gi://…` inside a diagnostic STRING containing the word
|
|
31
|
+
// `from`, and since an unmapped namespace fails the build, that made a
|
|
32
|
+
// correct project unbuildable.
|
|
33
|
+
//
|
|
34
|
+
// `walkModuleAst` answers both questions in ONE pass, and it is the same acorn
|
|
35
|
+
// pass the CLI already uses to compute its own runtime closure — so there is one
|
|
36
|
+
// definition of "what does this file import" rather than two.
|
|
37
|
+
//
|
|
38
|
+
// HARD OR OPTIONAL, since ADR 0087: a `gi://Ns?version=X&optional` import still
|
|
39
|
+
// appears in the emitted file, so this scanner still reports its namespace — and
|
|
40
|
+
// `depends.ts` has to know it may be absent. The discriminator is the marker
|
|
41
|
+
// statement the bundler's own optional shim emits (`GI_OPTIONAL_MARKER`, imported
|
|
42
|
+
// from the plugin that writes it rather than restated here). NOT the `import()`
|
|
43
|
+
// SHAPE, which was the tempting reading of the same file: an app that writes its own
|
|
44
|
+
// `await import('gi://Goa')` without the flag must stay a HARD dependency, and
|
|
45
|
+
// classifying it as optional would move a package the host must have out of its
|
|
46
|
+
// `Depends:` — the failure this whole chain exists to prevent, reached from the other
|
|
47
|
+
// direction.
|
|
48
|
+
import { GI_OPTIONAL_MARKER } from '../plugins/gi-optional.js';
|
|
49
|
+
import { importedSpecifier, staticStringValue, walkModuleAst } from './module-ast.js';
|
|
50
|
+
/** The module a `--app node` bundle reaches GI through. */
|
|
51
|
+
const NODE_GI_MODULE = '@gjsify/node-gi/gi';
|
|
52
|
+
/** Namespaces are GI identifiers; anything else is a call this reader misread. */
|
|
53
|
+
const NAMESPACE = /^[A-Za-z][A-Za-z\d_]*$/;
|
|
54
|
+
/**
|
|
55
|
+
* Extract the GI namespaces a bundle imports, as `Ns-Version` when the
|
|
56
|
+
* specifier pins one and bare `Ns` when it does not.
|
|
57
|
+
*/
|
|
58
|
+
export function scanGiNamespaces(source) {
|
|
59
|
+
return scanGiRequirements(source).namespaces;
|
|
60
|
+
}
|
|
61
|
+
/** {@link scanGiNamespaces}, plus which of them the artifact marks optional (ADR 0087). */
|
|
62
|
+
export function scanGiRequirements(source) {
|
|
63
|
+
const found = new Set();
|
|
64
|
+
const optional = new Set();
|
|
65
|
+
// TWO SETS, because the two kinds of binding are not interchangeable and
|
|
66
|
+
// treating them as one over-approximated on a real minified bundle. A
|
|
67
|
+
// namespace object (`import * as gi`) and a required module object are only
|
|
68
|
+
// ever the OBJECT of `.requireGi(…)`: calling a module namespace is a
|
|
69
|
+
// TypeError, so a bare `gi(…)` is never one of ours. Only `requireGi` itself
|
|
70
|
+
// — the named import, or the default export, which IS `requireGi` — is
|
|
71
|
+
// callable on its own.
|
|
72
|
+
//
|
|
73
|
+
// Measured on `gjsify build --app node` (minify is the DEFAULT): a file with
|
|
74
|
+
// `import * as gi from '@gjsify/node-gi/gi'` and a callback parameter emitted
|
|
75
|
+
// as `function render(e,t){return e(t)+e(\`Zzqfoo\`)}` — where the minifier
|
|
76
|
+
// gave the import and the parameter the SAME short name in different scopes.
|
|
77
|
+
// Read as one set, `e(\`Zzqfoo\`)` became the namespace `Zzqfoo`, and
|
|
78
|
+
// `deriveDepends` then refused to package a correct project.
|
|
79
|
+
const callable = new Set();
|
|
80
|
+
const objects = new Set();
|
|
81
|
+
const calls = [];
|
|
82
|
+
walkModuleAst(source, (node) => {
|
|
83
|
+
const specifier = importedSpecifier(node);
|
|
84
|
+
if (specifier !== null) {
|
|
85
|
+
const key = giSpecifierKey(specifier);
|
|
86
|
+
if (key !== null)
|
|
87
|
+
found.add(key);
|
|
88
|
+
if (node.type === 'ImportDeclaration' && specifier === NODE_GI_MODULE) {
|
|
89
|
+
collectNodeGiBindings(node, callable, objects);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (node.type === 'VariableDeclarator' && loadsNodeGiModule(node.init)) {
|
|
94
|
+
const id = node.id;
|
|
95
|
+
// An OBJECT: `const gi = require('@gjsify/node-gi/gi')` is the module,
|
|
96
|
+
// reached as `gi.requireGi(…)` and never called directly.
|
|
97
|
+
if (id?.type === 'Identifier' && typeof id.name === 'string')
|
|
98
|
+
objects.add(id.name);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Collected rather than resolved in place: a call can precede the
|
|
102
|
+
// `require` that binds its callee (the shim's `load()` is hoisted above
|
|
103
|
+
// nothing, but a minifier is free to reorder declarations), so the
|
|
104
|
+
// binding set has to be complete before any call is judged.
|
|
105
|
+
if (node.type === 'CallExpression')
|
|
106
|
+
calls.push(node);
|
|
107
|
+
});
|
|
108
|
+
for (const call of calls) {
|
|
109
|
+
const marker = optionalMarkerNamespace(call);
|
|
110
|
+
if (marker !== null) {
|
|
111
|
+
optional.add(marker);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const key = requireGiNamespace(call, callable, objects);
|
|
115
|
+
if (key !== null)
|
|
116
|
+
found.add(key);
|
|
117
|
+
}
|
|
118
|
+
return { namespaces: [...found].sort(), optional: [...optional].filter((ns) => found.has(ns)).sort() };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The namespace an optional-marker call names, or `null` when this is not one.
|
|
122
|
+
*
|
|
123
|
+
* The shape is the bundler's: `globalThis[Symbol.for(GI_OPTIONAL_MARKER)]?.("Ns", "X")`,
|
|
124
|
+
* matched on BOTH halves rather than on the whole statement — the minifier rewrites
|
|
125
|
+
* the string literals to backticks and the optional call to a `ChainExpression`
|
|
126
|
+
* around the `CallExpression` this walk still reaches, and a plain call (a host that
|
|
127
|
+
* has a handler the minifier could prove is non-null) reads the same way.
|
|
128
|
+
*/
|
|
129
|
+
function optionalMarkerNamespace(call) {
|
|
130
|
+
const callee = call.callee;
|
|
131
|
+
if (callee?.type !== 'MemberExpression' || callee.computed !== true)
|
|
132
|
+
return null;
|
|
133
|
+
// `globalThis[Symbol.for(KEY)]` — a computed member whose PROPERTY is the
|
|
134
|
+
// `Symbol.for` call, so the key is an argument of that inner call rather than
|
|
135
|
+
// of the outer one. Verified against acorn's own shape: `optional` chaining
|
|
136
|
+
// wraps the whole thing in a `ChainExpression`, which the walk descends into,
|
|
137
|
+
// so the `CallExpression` reaches this function either way.
|
|
138
|
+
const keyCall = callee.property;
|
|
139
|
+
if (keyCall?.type !== 'CallExpression')
|
|
140
|
+
return null;
|
|
141
|
+
const keyCallee = keyCall.callee;
|
|
142
|
+
const keyObject = keyCallee?.type === 'MemberExpression' ? keyCallee.object : undefined;
|
|
143
|
+
const keyProperty = keyCallee?.type === 'MemberExpression' ? keyCallee.property : undefined;
|
|
144
|
+
if (keyObject?.type !== 'Identifier' || keyObject.name !== 'Symbol')
|
|
145
|
+
return null;
|
|
146
|
+
if (keyProperty?.type !== 'Identifier' || keyProperty.name !== 'for')
|
|
147
|
+
return null;
|
|
148
|
+
const keyArgs = keyCall.arguments ?? [];
|
|
149
|
+
if (staticStringValue(keyArgs[0]) !== GI_OPTIONAL_MARKER)
|
|
150
|
+
return null;
|
|
151
|
+
const host = callee.object;
|
|
152
|
+
if (host?.type !== 'Identifier' || host.name !== 'globalThis')
|
|
153
|
+
return null;
|
|
154
|
+
const args = call.arguments ?? [];
|
|
155
|
+
const namespace = staticStringValue(args[0]);
|
|
156
|
+
if (namespace === null || !NAMESPACE.test(namespace))
|
|
157
|
+
return null;
|
|
158
|
+
const version = staticStringValue(args[1]);
|
|
159
|
+
return version === null || version === '' ? namespace : `${namespace}-${version}`;
|
|
160
|
+
}
|
|
161
|
+
/** `gi://Gtk?version=4.0` → `Gtk-4.0`; `gi://Gtk` → `Gtk`; anything else → null. */
|
|
162
|
+
export function giSpecifierKey(specifier) {
|
|
163
|
+
if (!specifier.startsWith('gi://'))
|
|
164
|
+
return null;
|
|
165
|
+
const rest = specifier.slice('gi://'.length);
|
|
166
|
+
const [namespace, query] = rest.split('?', 2);
|
|
167
|
+
if (namespace === undefined || !NAMESPACE.test(namespace))
|
|
168
|
+
return null;
|
|
169
|
+
if (query === undefined)
|
|
170
|
+
return namespace;
|
|
171
|
+
const version = /(?:^|&)version=([^&]+)/.exec(query)?.[1];
|
|
172
|
+
return version === undefined ? namespace : `${namespace}-${version}`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* What an `import … from '@gjsify/node-gi/gi'` binds, split by what it can BE.
|
|
176
|
+
*
|
|
177
|
+
* All three spellings appear in real code and the default export IS `requireGi`
|
|
178
|
+
* (`packages/node-gi/node-gi/gi.d.ts`), so the named import (under any local
|
|
179
|
+
* name — `import { requireGi as gi }` renames it) and the default import are
|
|
180
|
+
* CALLABLE. A namespace import is not: calling a module namespace object throws,
|
|
181
|
+
* so it can only ever be the object of `.requireGi(…)`, and putting it in the
|
|
182
|
+
* callable set is what let a minified callback parameter of the same name be
|
|
183
|
+
* read as a `requireGi` call.
|
|
184
|
+
*/
|
|
185
|
+
function collectNodeGiBindings(node, callable, objects) {
|
|
186
|
+
const specifiers = node.specifiers ?? [];
|
|
187
|
+
for (const specifier of specifiers) {
|
|
188
|
+
const local = specifier.local;
|
|
189
|
+
if (local?.type !== 'Identifier' || typeof local.name !== 'string')
|
|
190
|
+
continue;
|
|
191
|
+
const imported = specifier.imported;
|
|
192
|
+
if (specifier.type === 'ImportNamespaceSpecifier')
|
|
193
|
+
objects.add(local.name);
|
|
194
|
+
else if (specifier.type === 'ImportDefaultSpecifier')
|
|
195
|
+
callable.add(local.name);
|
|
196
|
+
else if (imported?.type === 'Identifier' && imported.name === 'requireGi')
|
|
197
|
+
callable.add(local.name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* A call that LOADS `@gjsify/node-gi/gi` — recognised by its argument, never by
|
|
202
|
+
* the name of the callee.
|
|
203
|
+
*
|
|
204
|
+
* THE CALLEE NAME DOES NOT SURVIVE BUNDLING, and reading it is how the first cut
|
|
205
|
+
* of this function passed its own tests while answering `[]` for every real
|
|
206
|
+
* artifact. Measured on `gjsify build --app node` output for a two-import app,
|
|
207
|
+
* which is the exact input this module exists to read:
|
|
208
|
+
*
|
|
209
|
+
* unminified const require$1 = createRequire(import.meta.url);
|
|
210
|
+
* … require$1("@gjsify/node-gi/gi").requireGi("Gtk", "4.0")
|
|
211
|
+
* minified const n = e(import.meta.url);
|
|
212
|
+
* … n(`@gjsify/node-gi/gi`).requireGi(`Gtk`, `4.0`)
|
|
213
|
+
*
|
|
214
|
+
* Two shims in one bundle means two `require` bindings, so the second is renamed
|
|
215
|
+
* even without `--minify` (which is the DEFAULT). A reader keyed on the
|
|
216
|
+
* identifier `require` therefore loses one namespace unminified and both
|
|
217
|
+
* minified — silently, since an empty namespace list is a legal answer.
|
|
218
|
+
*
|
|
219
|
+
* The module STRING is what survives every rename, and it is a precise
|
|
220
|
+
* discriminator: a call taking `'@gjsify/node-gi/gi'` as its first argument is a
|
|
221
|
+
* load of that module under any name a bundler gives the loader.
|
|
222
|
+
*/
|
|
223
|
+
function loadsNodeGiModule(node) {
|
|
224
|
+
if (!node || typeof node !== 'object')
|
|
225
|
+
return false;
|
|
226
|
+
const call = node;
|
|
227
|
+
if (call.type !== 'CallExpression')
|
|
228
|
+
return false;
|
|
229
|
+
const args = call.arguments ?? [];
|
|
230
|
+
return staticStringValue(args[0]) === NODE_GI_MODULE;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The namespace a `requireGi(…)` call names, or `null` when this is not one.
|
|
234
|
+
*
|
|
235
|
+
* BINDING-TRACED rather than name-matched, and the asymmetry is deliberate: a
|
|
236
|
+
* bare `.requireGi(…)` on any object would over-approximate, and
|
|
237
|
+
* over-approximating is NOT the harmless direction here — an unmapped namespace
|
|
238
|
+
* fails the build, so a foreign method sharing the name would make a correct
|
|
239
|
+
* project unpackageable. Every accepted shape traces back to an import or
|
|
240
|
+
* require of `@gjsify/node-gi/gi` in this same file, which is what a bundle
|
|
241
|
+
* always has: the shim carries its own `require`, and a hand-written node-gi app
|
|
242
|
+
* carries its own import.
|
|
243
|
+
*
|
|
244
|
+
* Shadowing is the one gap left, and the split above is what makes it small.
|
|
245
|
+
* This reader has no scope analysis, so a local binding that reuses the name of
|
|
246
|
+
* the CALLABLE import — `requireGi` itself, or the default import — and is called
|
|
247
|
+
* with a GI-shaped string literal would still be read as one. A minifier can
|
|
248
|
+
* manufacture exactly that collision, which is why the two shapes it produces
|
|
249
|
+
* most (the namespace object and the required module object) are the two that no
|
|
250
|
+
* longer count as callees.
|
|
251
|
+
*/
|
|
252
|
+
function requireGiNamespace(call, callable, objects) {
|
|
253
|
+
const callee = call.callee;
|
|
254
|
+
let reached = false;
|
|
255
|
+
if (callee?.type === 'Identifier') {
|
|
256
|
+
reached = typeof callee.name === 'string' && callable.has(callee.name);
|
|
257
|
+
}
|
|
258
|
+
else if (callee?.type === 'MemberExpression' && callee.computed !== true) {
|
|
259
|
+
const property = callee.property;
|
|
260
|
+
if (property?.type !== 'Identifier' || property.name !== 'requireGi')
|
|
261
|
+
return null;
|
|
262
|
+
const object = callee.object;
|
|
263
|
+
reached =
|
|
264
|
+
loadsNodeGiModule(object) ||
|
|
265
|
+
(object?.type === 'Identifier' && typeof object.name === 'string' && objects.has(object.name));
|
|
266
|
+
}
|
|
267
|
+
if (!reached)
|
|
268
|
+
return null;
|
|
269
|
+
const args = call.arguments ?? [];
|
|
270
|
+
const namespace = staticStringValue(args[0]);
|
|
271
|
+
if (namespace === null || !NAMESPACE.test(namespace))
|
|
272
|
+
return null;
|
|
273
|
+
const version = staticStringValue(args[1]);
|
|
274
|
+
return version === null ? namespace : `${namespace}-${version}`;
|
|
275
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse a `giNamespaces` entry that is not a GI namespace, naming the entry.
|
|
3
|
+
*
|
|
4
|
+
* ONE definition for both paths that accept the key — `gjsify build`'s config loader
|
|
5
|
+
* and `defineNativescriptConfig` — because a shape check that holds on one of them is
|
|
6
|
+
* a project that builds through Vite and fails through the CLI for a typo. A free-form
|
|
7
|
+
* string surfaces as "the prefix has no typelib for …" hours later in the AAR build,
|
|
8
|
+
* naming the sysroot rather than the mistake.
|
|
9
|
+
*
|
|
10
|
+
* @param where how the caller spells the key, so the message points at the file the
|
|
11
|
+
* reader is editing (`nativescript.giNamespaces` vs `giNamespaces`).
|
|
12
|
+
*/
|
|
13
|
+
export declare function assertGiNamespaceEntries(declared: readonly string[], where: string): void;
|
|
14
|
+
/** What {@link planGiSelection} decided, and what it wants the build to say out loud. */
|
|
15
|
+
export interface GiSelection {
|
|
16
|
+
/** The namespaces to hand the AAR builder, as the config spells them, deduped and sorted. */
|
|
17
|
+
selected: string[];
|
|
18
|
+
/** Declared but not found in the bundle — worth a warning, never a refusal. */
|
|
19
|
+
unscanned: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reconcile the configured namespace set with the one the emitted bundle imports.
|
|
23
|
+
*
|
|
24
|
+
* @param spec.declared `nativescript.giNamespaces`, as `Ns` or `Ns-Version`
|
|
25
|
+
* @param spec.scanned what {@link scanGiNamespaces} read off the emitted bundle
|
|
26
|
+
* @throws when the bundle imports a namespace the config does not cover
|
|
27
|
+
*/
|
|
28
|
+
export declare function planGiSelection({ declared, scanned }: {
|
|
29
|
+
declared: string[];
|
|
30
|
+
scanned: string[];
|
|
31
|
+
}): GiSelection;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Which GI namespaces does this Android APK carry? — ADR 0117 § 2, the CLI half.
|
|
2
|
+
//
|
|
3
|
+
// THE CONFIG DECIDES AND THE BUNDLE IS CHECKED AGAINST IT, which is the asymmetry
|
|
4
|
+
// the ADR turns on. The payload is cross-compiled hours earlier by
|
|
5
|
+
// `build-gi-runtime-android.mjs` against a sha256-pinned sysroot; nothing at app-build
|
|
6
|
+
// time can conjure a typelib that is not in it. So the config is the input, and the
|
|
7
|
+
// scan of the emitted bundle is the EVIDENCE that the input is complete.
|
|
8
|
+
//
|
|
9
|
+
// The scan is a LOWER BOUND, and the two directions of disagreement are therefore not
|
|
10
|
+
// symmetric:
|
|
11
|
+
//
|
|
12
|
+
// * IMPORTED, NOT CONFIGURED → build error naming the namespace. The scanner found a
|
|
13
|
+
// real import; the APK will not have the typelib; the device throws at that import,
|
|
14
|
+
// after the download, reading like an application bug. This is the same failure
|
|
15
|
+
// `deriveDepends` refuses for distro packages (ADR 0024 § 6), reached from Android.
|
|
16
|
+
// * CONFIGURED, NOT IMPORTED → warning. `scanGiNamespaces` reads the emitted file and
|
|
17
|
+
// cannot see a namespace reached through a computed specifier, nor one loaded by a
|
|
18
|
+
// native dependency. Refusing here would make a correct project unbuildable on the
|
|
19
|
+
// scanner's blind spot — so it reports a probably-larger-than-necessary APK and
|
|
20
|
+
// builds it.
|
|
21
|
+
//
|
|
22
|
+
// COMPARED BY NAMESPACE, NOT BY STRING. A config may pin an ABI (`Gtk-4.0`) where the
|
|
23
|
+
// bundle's specifier carries no version (`import 'gi://Gtk'`), and vice versa. Both
|
|
24
|
+
// sides mean the same namespace, and string equality would fail a correct project on
|
|
25
|
+
// the spelling.
|
|
26
|
+
/** `Gtk-4.0` → `Gtk`; `Gtk` → `Gtk`. */
|
|
27
|
+
function namespaceOf(entry) {
|
|
28
|
+
const dash = entry.indexOf('-');
|
|
29
|
+
return dash === -1 ? entry : entry.slice(0, dash);
|
|
30
|
+
}
|
|
31
|
+
/** `Ns` or `Ns-Version` — what the AAR builder resolves against the sysroot's typelib names. */
|
|
32
|
+
const GI_SELECTION_ENTRY = /^[A-Za-z][A-Za-z\d_]*(-\d+(\.\d+)*)?$/;
|
|
33
|
+
/**
|
|
34
|
+
* Refuse a `giNamespaces` entry that is not a GI namespace, naming the entry.
|
|
35
|
+
*
|
|
36
|
+
* ONE definition for both paths that accept the key — `gjsify build`'s config loader
|
|
37
|
+
* and `defineNativescriptConfig` — because a shape check that holds on one of them is
|
|
38
|
+
* a project that builds through Vite and fails through the CLI for a typo. A free-form
|
|
39
|
+
* string surfaces as "the prefix has no typelib for …" hours later in the AAR build,
|
|
40
|
+
* naming the sysroot rather than the mistake.
|
|
41
|
+
*
|
|
42
|
+
* @param where how the caller spells the key, so the message points at the file the
|
|
43
|
+
* reader is editing (`nativescript.giNamespaces` vs `giNamespaces`).
|
|
44
|
+
*/
|
|
45
|
+
export function assertGiNamespaceEntries(declared, where) {
|
|
46
|
+
for (const entry of declared) {
|
|
47
|
+
if (!GI_SELECTION_ENTRY.test(entry)) {
|
|
48
|
+
throw new Error(`${where} entry "${entry}" is not a GI namespace. Write it as \`Gtk\` or \`Gtk-4.0\`.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The namespaces every AAR carries whether or not anything declares them.
|
|
54
|
+
*
|
|
55
|
+
* These back `libnode_gi.so` itself, so they are in the payload's DT_NEEDED closure
|
|
56
|
+
* before any selection happens (ADR 0110's measured core set). Demanding them in the
|
|
57
|
+
* config would make the key boilerplate and fail every project that exists today, for
|
|
58
|
+
* a selection that cannot change the bytes.
|
|
59
|
+
*/
|
|
60
|
+
const ALWAYS_PRESENT = new Set(['GLib', 'GObject', 'Gio', 'GioUnix', 'GLibUnix', 'GModule', 'GIRepository']);
|
|
61
|
+
/**
|
|
62
|
+
* Reconcile the configured namespace set with the one the emitted bundle imports.
|
|
63
|
+
*
|
|
64
|
+
* @param spec.declared `nativescript.giNamespaces`, as `Ns` or `Ns-Version`
|
|
65
|
+
* @param spec.scanned what {@link scanGiNamespaces} read off the emitted bundle
|
|
66
|
+
* @throws when the bundle imports a namespace the config does not cover
|
|
67
|
+
*/
|
|
68
|
+
export function planGiSelection({ declared, scanned }) {
|
|
69
|
+
// A bare and a pinned declaration of one namespace are one selection, and the
|
|
70
|
+
// pinned spelling wins: it says something the bare one does not.
|
|
71
|
+
const byNamespace = new Map();
|
|
72
|
+
for (const entry of declared) {
|
|
73
|
+
const ns = namespaceOf(entry);
|
|
74
|
+
const held = byNamespace.get(ns);
|
|
75
|
+
if (held === undefined || (held === ns && entry !== ns))
|
|
76
|
+
byNamespace.set(ns, entry);
|
|
77
|
+
}
|
|
78
|
+
const missing = [
|
|
79
|
+
...new Set(scanned.map(namespaceOf).filter((ns) => !byNamespace.has(ns) && !ALWAYS_PRESENT.has(ns))),
|
|
80
|
+
].sort();
|
|
81
|
+
if (missing.length > 0) {
|
|
82
|
+
throw new Error(`The bundle imports ${missing.join(', ')}, which nativescript.giNamespaces does not declare. ` +
|
|
83
|
+
'An Android APK carries only the typelibs the config selects (ADR 0117), so this would ' +
|
|
84
|
+
`install and then throw at the import. Add ${missing.length === 1 ? 'it' : 'them'} to ` +
|
|
85
|
+
'nativescript.giNamespaces, or stop importing ' +
|
|
86
|
+
`${missing.length === 1 ? 'it' : 'them'}.`);
|
|
87
|
+
}
|
|
88
|
+
const seen = new Set(scanned.map(namespaceOf));
|
|
89
|
+
const unscanned = [...byNamespace.entries()]
|
|
90
|
+
.filter(([ns]) => !seen.has(ns))
|
|
91
|
+
.map(([, entry]) => entry)
|
|
92
|
+
.sort();
|
|
93
|
+
return { selected: [...byNamespace.values()].sort(), unscanned };
|
|
94
|
+
}
|
package/lib/utils/index.d.ts
CHANGED
|
@@ -10,3 +10,9 @@ export { GJSIFY_VIRTUAL_PREFIX, isGjsifyVirtualModuleId } from './virtual-module
|
|
|
10
10
|
export { locateSurvivingJsx, classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
11
11
|
export type { SurvivingJsx } from './jsx-survival.js';
|
|
12
12
|
export { wrapInputWithSideEffects } from './entry-wrapper.js';
|
|
13
|
+
export { staticStringValue, walkModuleAst, importedSpecifier, moduleSpecifiers } from './module-ast.js';
|
|
14
|
+
export type { SpecifierNode, AstNode } from './module-ast.js';
|
|
15
|
+
export { scanGiNamespaces, scanGiRequirements, giSpecifierKey } from './gi-namespaces.js';
|
|
16
|
+
export type { GiRequirements } from './gi-namespaces.js';
|
|
17
|
+
export { planGiSelection, assertGiNamespaceEntries } from './gi-selection.js';
|
|
18
|
+
export type { GiSelection } from './gi-selection.js';
|
package/lib/utils/index.js
CHANGED
|
@@ -9,3 +9,6 @@ export { inlineStaticReads, isAbsoluteFsPath, isWithin, resourceRootFor } from '
|
|
|
9
9
|
export { GJSIFY_VIRTUAL_PREFIX, isGjsifyVirtualModuleId } from './virtual-module-id.js';
|
|
10
10
|
export { locateSurvivingJsx, classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
11
11
|
export { wrapInputWithSideEffects } from './entry-wrapper.js';
|
|
12
|
+
export { staticStringValue, walkModuleAst, importedSpecifier, moduleSpecifiers } from './module-ast.js';
|
|
13
|
+
export { scanGiNamespaces, scanGiRequirements, giSpecifierKey } from './gi-namespaces.js';
|
|
14
|
+
export { planGiSelection, assertGiNamespaceEntries } from './gi-selection.js';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Literal module specifiers of `code`: static `import`/`export … from`, plus
|
|
3
|
+
* `import('<literal>')`.
|
|
4
|
+
*
|
|
5
|
+
* acorn rather than a regex — see the header. `allowHashBang` because a CLI's
|
|
6
|
+
* own entry is a bin (`#!/usr/bin/env node`), and a parse failure yields NO
|
|
7
|
+
* specifiers, which under-selects rather than mis-selects.
|
|
8
|
+
*/
|
|
9
|
+
export interface SpecifierNode {
|
|
10
|
+
type?: string;
|
|
11
|
+
value?: unknown;
|
|
12
|
+
expressions?: unknown[];
|
|
13
|
+
quasis?: Array<{
|
|
14
|
+
value?: {
|
|
15
|
+
cooked?: unknown;
|
|
16
|
+
};
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The string a node names statically, or `null` when it is computed.
|
|
21
|
+
*
|
|
22
|
+
* A substitution-free TEMPLATE LITERAL is as static as a quoted string, and
|
|
23
|
+
* skipping it is not hypothetical: the minifier rewrites `await
|
|
24
|
+
* import('gi://GLib?version=2.0')` to a backtick form, so a Literal-only reader
|
|
25
|
+
* silently loses every dynamic import in a built bundle.
|
|
26
|
+
*
|
|
27
|
+
* Not named for specifiers, because it does not only read them: the same
|
|
28
|
+
* question is asked of a CALL ARGUMENT by `gi-namespaces.ts`, whose
|
|
29
|
+
* `requireGi("Gtk", "4.0")` carries a namespace and a version in exactly the two
|
|
30
|
+
* spellings above.
|
|
31
|
+
*/
|
|
32
|
+
export declare function staticStringValue(node: SpecifierNode | undefined): string | null;
|
|
33
|
+
/** One node of the walk: whatever acorn produced, seen as a bag of properties. */
|
|
34
|
+
export type AstNode = {
|
|
35
|
+
type?: string;
|
|
36
|
+
} & Record<string, unknown>;
|
|
37
|
+
/**
|
|
38
|
+
* Parse `code` as a module and call `visit` for every node in it.
|
|
39
|
+
*
|
|
40
|
+
* Exported because a SECOND parse is how the readers of a built bundle would drift
|
|
41
|
+
* apart: {@link moduleSpecifiers} answers "what does this file import" and
|
|
42
|
+
* `gi-namespaces.ts` answers "which GI namespaces does it load", and the second
|
|
43
|
+
* question has an answer the first cannot see — a `--app node` bundle carries no
|
|
44
|
+
* `gi://` specifier at all, only the `requireGi()` calls the bundler rewrote them
|
|
45
|
+
* into. One walker, many readers.
|
|
46
|
+
*
|
|
47
|
+
* A parse failure yields NO nodes rather than throwing: every caller is
|
|
48
|
+
* summarising a file it did not write, and under-selecting is the failure mode
|
|
49
|
+
* that keeps a correct project buildable.
|
|
50
|
+
*/
|
|
51
|
+
export declare function walkModuleAst(code: string, visit: (node: AstNode) => void): void;
|
|
52
|
+
/**
|
|
53
|
+
* The module `node` imports from, or `null` when `node` is not an import at all.
|
|
54
|
+
*
|
|
55
|
+
* FOUR node types, in one place. Every reader of a built bundle in this tree asks
|
|
56
|
+
* the same question of the same four — a static `import`, both `export … from`
|
|
57
|
+
* forms and `import()` — and each copy of that list is a chance for one reader to
|
|
58
|
+
* miss a form the others catch. The bare side-effect `import "gi://Soup"` is what
|
|
59
|
+
* that costs when it happens (`gi-namespaces.ts`).
|
|
60
|
+
*/
|
|
61
|
+
export declare function importedSpecifier(node: AstNode): string | null;
|
|
62
|
+
export declare function moduleSpecifiers(code: string): string[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// One acorn walk over a built module, for every reader in the tree that has to
|
|
2
|
+
// summarise a file it did not write.
|
|
3
|
+
//
|
|
4
|
+
// WHY HERE AND NOT IN THE CLI, where these four functions were born: the readers
|
|
5
|
+
// are no longer all in the CLI. `gjsify build --app nativescript` asks which GI
|
|
6
|
+
// namespaces its bundle loads, and so does the Vite path (`defineNativescriptConfig`,
|
|
7
|
+
// ADR 0117 § stage 1b), which never boots the CLI at all. The bundler-plugin package
|
|
8
|
+
// is what both of those depend on, so it is where the walk can be ONE walk — and a
|
|
9
|
+
// second parse in the Vite path is exactly how the two answers would drift.
|
|
10
|
+
//
|
|
11
|
+
// A parse failure yields NO nodes rather than throwing: every caller is summarising
|
|
12
|
+
// somebody else's artifact, and under-selecting keeps a correct project buildable
|
|
13
|
+
// where mis-selecting breaks one.
|
|
14
|
+
import * as acorn from 'acorn';
|
|
15
|
+
/**
|
|
16
|
+
* The string a node names statically, or `null` when it is computed.
|
|
17
|
+
*
|
|
18
|
+
* A substitution-free TEMPLATE LITERAL is as static as a quoted string, and
|
|
19
|
+
* skipping it is not hypothetical: the minifier rewrites `await
|
|
20
|
+
* import('gi://GLib?version=2.0')` to a backtick form, so a Literal-only reader
|
|
21
|
+
* silently loses every dynamic import in a built bundle.
|
|
22
|
+
*
|
|
23
|
+
* Not named for specifiers, because it does not only read them: the same
|
|
24
|
+
* question is asked of a CALL ARGUMENT by `gi-namespaces.ts`, whose
|
|
25
|
+
* `requireGi("Gtk", "4.0")` carries a namespace and a version in exactly the two
|
|
26
|
+
* spellings above.
|
|
27
|
+
*/
|
|
28
|
+
export function staticStringValue(node) {
|
|
29
|
+
if (node?.type === 'Literal')
|
|
30
|
+
return typeof node.value === 'string' ? node.value : null;
|
|
31
|
+
if (node?.type === 'TemplateLiteral' && node.expressions?.length === 0) {
|
|
32
|
+
const cooked = node.quasis?.[0]?.value?.cooked;
|
|
33
|
+
return typeof cooked === 'string' ? cooked : null;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse `code` as a module and call `visit` for every node in it.
|
|
39
|
+
*
|
|
40
|
+
* Exported because a SECOND parse is how the readers of a built bundle would drift
|
|
41
|
+
* apart: {@link moduleSpecifiers} answers "what does this file import" and
|
|
42
|
+
* `gi-namespaces.ts` answers "which GI namespaces does it load", and the second
|
|
43
|
+
* question has an answer the first cannot see — a `--app node` bundle carries no
|
|
44
|
+
* `gi://` specifier at all, only the `requireGi()` calls the bundler rewrote them
|
|
45
|
+
* into. One walker, many readers.
|
|
46
|
+
*
|
|
47
|
+
* A parse failure yields NO nodes rather than throwing: every caller is
|
|
48
|
+
* summarising a file it did not write, and under-selecting is the failure mode
|
|
49
|
+
* that keeps a correct project buildable.
|
|
50
|
+
*/
|
|
51
|
+
export function walkModuleAst(code, visit) {
|
|
52
|
+
let ast;
|
|
53
|
+
try {
|
|
54
|
+
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const descend = (node) => {
|
|
60
|
+
if (!node || typeof node !== 'object')
|
|
61
|
+
return;
|
|
62
|
+
if (Array.isArray(node)) {
|
|
63
|
+
for (const child of node)
|
|
64
|
+
descend(child);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const n = node;
|
|
68
|
+
visit(n);
|
|
69
|
+
for (const [key, value] of Object.entries(n)) {
|
|
70
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc')
|
|
71
|
+
continue;
|
|
72
|
+
descend(value);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
descend(ast);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The module `node` imports from, or `null` when `node` is not an import at all.
|
|
79
|
+
*
|
|
80
|
+
* FOUR node types, in one place. Every reader of a built bundle in this tree asks
|
|
81
|
+
* the same question of the same four — a static `import`, both `export … from`
|
|
82
|
+
* forms and `import()` — and each copy of that list is a chance for one reader to
|
|
83
|
+
* miss a form the others catch. The bare side-effect `import "gi://Soup"` is what
|
|
84
|
+
* that costs when it happens (`gi-namespaces.ts`).
|
|
85
|
+
*/
|
|
86
|
+
export function importedSpecifier(node) {
|
|
87
|
+
if (node.type !== 'ImportDeclaration' &&
|
|
88
|
+
node.type !== 'ExportNamedDeclaration' &&
|
|
89
|
+
node.type !== 'ExportAllDeclaration' &&
|
|
90
|
+
node.type !== 'ImportExpression') {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return staticStringValue(node.source);
|
|
94
|
+
}
|
|
95
|
+
export function moduleSpecifiers(code) {
|
|
96
|
+
const out = [];
|
|
97
|
+
walkModuleAst(code, (node) => {
|
|
98
|
+
const specifier = importedSpecifier(node);
|
|
99
|
+
if (specifier !== null)
|
|
100
|
+
out.push(specifier);
|
|
101
|
+
});
|
|
102
|
+
return out;
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/rolldown-plugin-gjsify",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.13-next.283",
|
|
4
4
|
"description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -67,12 +67,13 @@
|
|
|
67
67
|
],
|
|
68
68
|
"license": "MIT",
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@gjsify/console": "
|
|
71
|
-
"@gjsify/resolve-npm": "
|
|
72
|
-
"@gjsify/rolldown-plugin-deepkit": "
|
|
73
|
-
"@gjsify/rolldown-plugin-pnp": "
|
|
74
|
-
"@gjsify/utils": "
|
|
75
|
-
"@gjsify/vite-plugin-blueprint": "
|
|
70
|
+
"@gjsify/console": "0.64.13-next.283",
|
|
71
|
+
"@gjsify/resolve-npm": "0.64.13-next.283",
|
|
72
|
+
"@gjsify/rolldown-plugin-deepkit": "0.64.13-next.283",
|
|
73
|
+
"@gjsify/rolldown-plugin-pnp": "0.64.13-next.283",
|
|
74
|
+
"@gjsify/utils": "0.64.13-next.283",
|
|
75
|
+
"@gjsify/vite-plugin-blueprint": "0.64.13-next.283",
|
|
76
|
+
"@gjsify/vite-plugin-gettext": "0.64.13-next.283",
|
|
76
77
|
"@rollup/pluginutils": "^5.4.0",
|
|
77
78
|
"acorn": "^8.17.0",
|
|
78
79
|
"acorn-typescript": "^1.4.13",
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
"sass": "^1.101.0"
|
|
83
84
|
},
|
|
84
85
|
"peerDependencies": {
|
|
85
|
-
"@gjsify/lightningcss-native": "
|
|
86
|
+
"@gjsify/lightningcss-native": "0.64.13-next.283",
|
|
86
87
|
"rolldown": "^1.1.4"
|
|
87
88
|
},
|
|
88
89
|
"peerDependenciesMeta": {
|
|
@@ -94,8 +95,8 @@
|
|
|
94
95
|
}
|
|
95
96
|
},
|
|
96
97
|
"devDependencies": {
|
|
97
|
-
"@gjsify/cli": "
|
|
98
|
-
"@gjsify/unit": "
|
|
98
|
+
"@gjsify/cli": "0.64.13-next.283",
|
|
99
|
+
"@gjsify/unit": "0.64.13-next.283",
|
|
99
100
|
"@types/node": "^25.9.2",
|
|
100
101
|
"rolldown": "^1.1.4",
|
|
101
102
|
"typescript": "^6.0.3"
|
|
@@ -107,5 +108,6 @@
|
|
|
107
108
|
"darwin": "supported",
|
|
108
109
|
"win32": "supported"
|
|
109
110
|
}
|
|
110
|
-
}
|
|
111
|
+
},
|
|
112
|
+
"gitHead": "05fae97125a374e84bf57c2d2d086fb53523f205"
|
|
111
113
|
}
|