@fairgarden/monolith 0.1.0-0.canary.0
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/Readme.md +84 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +160 -0
- package/dist/cli.js.map +1 -0
- package/dist/compat.d.ts +4 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +174 -0
- package/dist/compat.js.map +1 -0
- package/dist/eslint.d.ts +62 -0
- package/dist/eslint.d.ts.map +1 -0
- package/dist/eslint.js +198 -0
- package/dist/eslint.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/link.d.ts +24 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +473 -0
- package/dist/link.js.map +1 -0
- package/dist/mounts.d.ts +37 -0
- package/dist/mounts.d.ts.map +1 -0
- package/dist/mounts.js +58 -0
- package/dist/mounts.js.map +1 -0
- package/dist/package-json.d.ts +38 -0
- package/dist/package-json.d.ts.map +1 -0
- package/dist/package-json.js +156 -0
- package/dist/package-json.js.map +1 -0
- package/dist/portability.d.ts +41 -0
- package/dist/portability.d.ts.map +1 -0
- package/dist/portability.js +171 -0
- package/dist/portability.js.map +1 -0
- package/dist/portable-link.d.ts +22 -0
- package/dist/portable-link.d.ts.map +1 -0
- package/dist/portable-link.js +24 -0
- package/dist/portable-link.js.map +1 -0
- package/dist/proxies.d.ts +33 -0
- package/dist/proxies.d.ts.map +1 -0
- package/dist/proxies.js +177 -0
- package/dist/proxies.js.map +1 -0
- package/dist/resolve.d.ts +5 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +181 -0
- package/dist/resolve.js.map +1 -0
- package/dist/routes.d.ts +23 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +102 -0
- package/dist/routes.js.map +1 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { isBuiltin } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import mergePackageJson from 'merge-package.json';
|
|
5
|
+
const DEPENDENCY_KEYS = [
|
|
6
|
+
'dependencies',
|
|
7
|
+
'devDependencies',
|
|
8
|
+
'optionalDependencies',
|
|
9
|
+
'peerDependencies',
|
|
10
|
+
];
|
|
11
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
12
|
+
'.ts',
|
|
13
|
+
'.tsx',
|
|
14
|
+
'.js',
|
|
15
|
+
'.jsx',
|
|
16
|
+
'.mjs',
|
|
17
|
+
'.cjs',
|
|
18
|
+
]);
|
|
19
|
+
/** `import x from 'y'`, `import 'y'`, `import('y')` and `require('y')`. */
|
|
20
|
+
const SPECIFIER = /(?:from|import|require)\s*\(?\s*['"]([^'"]+)['"]/g;
|
|
21
|
+
const read = async (file) => JSON.parse(await readFile(file, 'utf8'));
|
|
22
|
+
/** `@scope/name/sub` -> `@scope/name`, `name/sub` -> `name`. */
|
|
23
|
+
const packageOf = (specifier) => {
|
|
24
|
+
const segments = specifier.split('/');
|
|
25
|
+
return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
|
|
26
|
+
};
|
|
27
|
+
const sourceFiles = async (dir) => {
|
|
28
|
+
const found = [];
|
|
29
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.name === 'node_modules')
|
|
32
|
+
continue;
|
|
33
|
+
const full = path.join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
found.push(...(await sourceFiles(full)));
|
|
36
|
+
}
|
|
37
|
+
else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
38
|
+
found.push(full);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return found;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Packages the app's route files import directly.
|
|
45
|
+
*
|
|
46
|
+
* Both route trees need these from the monolith, for different reasons. Pages
|
|
47
|
+
* Router files are copied, so their real path is inside the monolith and the
|
|
48
|
+
* app's `node_modules` is no longer above them. App Router files are only
|
|
49
|
+
* symlinked, and Turbopack does resolve those against the app — but TypeScript
|
|
50
|
+
* resolves from where the file sits in the monolith, so it needs them too.
|
|
51
|
+
*
|
|
52
|
+
* Only *direct* imports matter. Anything an app reaches through its own package
|
|
53
|
+
* name resolves via the monolith's link to the app and carries on from there.
|
|
54
|
+
*/
|
|
55
|
+
export const routeImports = async (app) => {
|
|
56
|
+
const imported = new Set();
|
|
57
|
+
const roots = [app.appDir, app.pagesDir].filter((root) => root !== undefined);
|
|
58
|
+
for (const file of (await Promise.all(roots.map(sourceFiles))).flat()) {
|
|
59
|
+
const contents = await readFile(file, 'utf8').catch(() => '');
|
|
60
|
+
for (const [, specifier] of contents.matchAll(SPECIFIER)) {
|
|
61
|
+
if (specifier.startsWith('.') || isBuiltin(specifier))
|
|
62
|
+
continue;
|
|
63
|
+
const name = packageOf(specifier);
|
|
64
|
+
// Resolves through the monolith's link to the app, not its own deps.
|
|
65
|
+
if (name === app.packageName)
|
|
66
|
+
continue;
|
|
67
|
+
imported.add(name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return imported;
|
|
71
|
+
};
|
|
72
|
+
/** `@scope/name` -> `@types/scope__name`, matching DefinitelyTyped's layout. */
|
|
73
|
+
const typesPackageOf = (name) => name.startsWith('@')
|
|
74
|
+
? `@types/${name.slice(1).replace('/', '__')}`
|
|
75
|
+
: `@types/${name}`;
|
|
76
|
+
/**
|
|
77
|
+
* Workspace siblings are linked, not installed, so carrying their version
|
|
78
|
+
* ranges into the monolith would pin it against its own workspace.
|
|
79
|
+
*/
|
|
80
|
+
const isInstallable = (range) => typeof range === 'string' &&
|
|
81
|
+
!range.startsWith('workspace:') &&
|
|
82
|
+
!range.startsWith('link:');
|
|
83
|
+
const pick = (pkg, wanted) => {
|
|
84
|
+
const picked = {};
|
|
85
|
+
for (const key of DEPENDENCY_KEYS) {
|
|
86
|
+
const deps = pkg[key];
|
|
87
|
+
if (!deps)
|
|
88
|
+
continue;
|
|
89
|
+
const taken = Object.entries(deps).filter(([name, range]) => wanted.has(name) && isInstallable(range));
|
|
90
|
+
if (taken.length > 0)
|
|
91
|
+
picked[key] = Object.fromEntries(taken);
|
|
92
|
+
}
|
|
93
|
+
return picked;
|
|
94
|
+
};
|
|
95
|
+
const everything = (pkg) => {
|
|
96
|
+
const picked = {};
|
|
97
|
+
for (const key of DEPENDENCY_KEYS) {
|
|
98
|
+
const deps = pkg[key];
|
|
99
|
+
if (!deps)
|
|
100
|
+
continue;
|
|
101
|
+
const taken = Object.entries(deps).filter(([, range]) => isInstallable(range));
|
|
102
|
+
if (taken.length > 0)
|
|
103
|
+
picked[key] = Object.fromEntries(taken);
|
|
104
|
+
}
|
|
105
|
+
return picked;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Fold what each app needs from the monolith into its package.json.
|
|
109
|
+
*
|
|
110
|
+
* Uses a three-way merge against an empty base so conflicting ranges are
|
|
111
|
+
* resolved by semver rather than by whichever app happens to be last.
|
|
112
|
+
*/
|
|
113
|
+
export const mergeAppDependencies = async (monolithPackageJsonPath, apps, options = {}) => {
|
|
114
|
+
const original = await read(monolithPackageJsonPath);
|
|
115
|
+
let merged = JSON.stringify(original);
|
|
116
|
+
const taken = new Map();
|
|
117
|
+
for (const app of apps) {
|
|
118
|
+
const appPkg = await read(path.join(app.root, 'package.json'));
|
|
119
|
+
let incoming;
|
|
120
|
+
if (options.all) {
|
|
121
|
+
incoming = everything(appPkg);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const wanted = await routeImports(app);
|
|
125
|
+
// Type declarations are resolved from the monolith too.
|
|
126
|
+
for (const name of [...wanted])
|
|
127
|
+
wanted.add(typesPackageOf(name));
|
|
128
|
+
incoming = pick(appPkg, wanted);
|
|
129
|
+
}
|
|
130
|
+
const names = DEPENDENCY_KEYS.flatMap((key) => Object.keys(incoming[key] ?? {}));
|
|
131
|
+
if (names.length === 0)
|
|
132
|
+
continue;
|
|
133
|
+
taken.set(app.name, names);
|
|
134
|
+
merged = mergePackageJson(merged, JSON.stringify({}), JSON.stringify(incoming));
|
|
135
|
+
}
|
|
136
|
+
const packageJson = JSON.parse(merged);
|
|
137
|
+
return {
|
|
138
|
+
packageJson,
|
|
139
|
+
// Compared by content, not by key order: the merge re-sorts, and a
|
|
140
|
+
// monolith whose dependencies sit in any other order would otherwise fail
|
|
141
|
+
// `--check` on every run with nothing to actually change.
|
|
142
|
+
changed: !sameDependencies(original, packageJson),
|
|
143
|
+
taken,
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
/** Whether two manifests declare the same dependencies, order aside. */
|
|
147
|
+
const sameDependencies = (a, b) => DEPENDENCY_KEYS.every((key) => {
|
|
148
|
+
const left = (a[key] ?? {});
|
|
149
|
+
const right = (b[key] ?? {});
|
|
150
|
+
const names = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
151
|
+
return [...names].every((name) => left[name] === right[name]);
|
|
152
|
+
});
|
|
153
|
+
export const writePackageJson = async (file, packageJson) => {
|
|
154
|
+
await writeFile(file, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
155
|
+
};
|
|
156
|
+
//# sourceMappingURL=package-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-json.js","sourceRoot":"","sources":["../src/package-json.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { NextConfig } from 'next';
|
|
2
|
+
export interface PortabilityOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The app to check. Defaults to the directory of the config that called
|
|
5
|
+
* this, not the working directory — a monolith loading an app's config
|
|
6
|
+
* would otherwise have the app checked against the monolith's own files.
|
|
7
|
+
*/
|
|
8
|
+
root?: string;
|
|
9
|
+
/** `warn` reports and continues, `error` refuses to build. Defaults to `warn`. */
|
|
10
|
+
level?: 'warn' | 'error';
|
|
11
|
+
/** Findings to accept, by the text before the colon. */
|
|
12
|
+
ignore?: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Everything about this app that would not survive being mounted in a monolith.
|
|
16
|
+
*
|
|
17
|
+
* Exported separately so a test or a CI check can assert on the list rather
|
|
18
|
+
* than scrape log output.
|
|
19
|
+
*/
|
|
20
|
+
export declare const findPortabilityProblems: (nextConfig: NextConfig, root: string) => string[];
|
|
21
|
+
/**
|
|
22
|
+
* Write a report once per build, however many processes load the config:
|
|
23
|
+
* each build worker inherits the environment this marks.
|
|
24
|
+
*/
|
|
25
|
+
export declare const reportOnce: (key: string, report: string) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Check that an app can be mounted into a monolith, from the app's own config.
|
|
28
|
+
*
|
|
29
|
+
* Wrap the app's config with this and it reports, on every build, whatever
|
|
30
|
+
* would not survive being served under a prefix alongside other apps. It
|
|
31
|
+
* changes nothing about how the app runs on its own.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* // apps/id/next.config.ts
|
|
35
|
+
* export default withMonolithicPortability({ redirects: async () => [...] })
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* Set `level: 'error'` in CI to make it a gate rather than a reminder.
|
|
39
|
+
*/
|
|
40
|
+
export declare const withMonolithicPortability: (nextConfig?: NextConfig, options?: PortabilityOptions) => NextConfig;
|
|
41
|
+
//# sourceMappingURL=portability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portability.d.ts","sourceRoot":"","sources":["../src/portability.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAyEtC,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACxB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAClC,YAAY,UAAU,EACtB,MAAM,MAAM,KACX,MAAM,EAqCR,CAAA;AA0CD;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,EAAE,QAAQ,MAAM,KAAG,IAIxD,CAAA;AASD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,GACpC,aAAY,UAAe,EAC3B,UAAS,kBAAuB,KAC/B,UAcF,CAAA"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isLocaleProxy } from "./proxies.js";
|
|
4
|
+
/**
|
|
5
|
+
* Options the monolith owns for the whole deployment. An app that sets one is
|
|
6
|
+
* either fighting the monolith or silently changing every other app.
|
|
7
|
+
*/
|
|
8
|
+
const OWNED_OPTIONS = ['basePath', 'distDir'];
|
|
9
|
+
/** Options that have to agree across every app, because one server serves all. */
|
|
10
|
+
const SHARED_OPTIONS = [
|
|
11
|
+
'i18n',
|
|
12
|
+
'trailingSlash',
|
|
13
|
+
'skipTrailingSlashRedirect',
|
|
14
|
+
'skipMiddlewareUrlNormalize',
|
|
15
|
+
'output',
|
|
16
|
+
'assetPrefix',
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Conventions Next allows exactly one of per deployment. An app can use them on
|
|
20
|
+
* its own, but mounting several apps means only one could ever win — so rather
|
|
21
|
+
* than let one app's copy silently disappear, say so.
|
|
22
|
+
*/
|
|
23
|
+
const SINGLETON_FILES = [
|
|
24
|
+
{
|
|
25
|
+
names: ['proxy', 'middleware', 'src/proxy', 'src/middleware'],
|
|
26
|
+
why: 'a deployment has one proxy; the monolith would have to run it and strip the mount prefix itself',
|
|
27
|
+
// The locale proxy from @fairgarden/indicators takes each app by its
|
|
28
|
+
// mount, so the monolith's own proxy runs it for this app too.
|
|
29
|
+
portable: isLocaleProxy,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
names: ['instrumentation', 'src/instrumentation'],
|
|
33
|
+
why: 'a deployment has one instrumentation hook',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
names: ['pages/_app', 'src/pages/_app'],
|
|
37
|
+
why: 'a deployment has one Pages Router app shell',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
names: ['pages/_document', 'src/pages/_document'],
|
|
41
|
+
why: 'a deployment has one Pages Router document',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* Files that generate a route which only means anything at the site root.
|
|
46
|
+
* Mounted, they end up at `/<name>/robots.txt`, where nothing looks for them.
|
|
47
|
+
*/
|
|
48
|
+
const ROOT_ONLY_ROUTES = ['robots', 'sitemap', 'manifest'];
|
|
49
|
+
const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs'];
|
|
50
|
+
const exists = (candidate) => {
|
|
51
|
+
try {
|
|
52
|
+
statSync(candidate);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const findFile = (root, name) => EXTENSIONS.map((extension) => `${name}${extension}`).find((file) => exists(path.join(root, file)));
|
|
60
|
+
/**
|
|
61
|
+
* Everything about this app that would not survive being mounted in a monolith.
|
|
62
|
+
*
|
|
63
|
+
* Exported separately so a test or a CI check can assert on the list rather
|
|
64
|
+
* than scrape log output.
|
|
65
|
+
*/
|
|
66
|
+
export const findPortabilityProblems = (nextConfig, root) => {
|
|
67
|
+
const problems = [];
|
|
68
|
+
for (const option of OWNED_OPTIONS) {
|
|
69
|
+
if (nextConfig[option] !== undefined) {
|
|
70
|
+
problems.push(`${option}: only the monolith may set this; mounted apps are served from their mount point instead`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const option of SHARED_OPTIONS) {
|
|
74
|
+
if (nextConfig[option] !== undefined) {
|
|
75
|
+
problems.push(`${option}: has to match every other app in the monolith, so it belongs on the monolith`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const { names, why, portable } of SINGLETON_FILES) {
|
|
79
|
+
const found = names.map((name) => findFile(root, name)).find(Boolean);
|
|
80
|
+
if (found && portable?.(path.join(root, found)))
|
|
81
|
+
continue;
|
|
82
|
+
if (found)
|
|
83
|
+
problems.push(`${found}: ${why}`);
|
|
84
|
+
}
|
|
85
|
+
for (const name of ROOT_ONLY_ROUTES) {
|
|
86
|
+
const found = findFile(root, path.join('app', name)) ??
|
|
87
|
+
findFile(root, path.join('src', 'app', name));
|
|
88
|
+
if (found) {
|
|
89
|
+
problems.push(`${found}: serves a route that only works at the site root, and would be mounted under the app's prefix`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return problems;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Next reads the config in the main process and again in a build worker.
|
|
96
|
+
* Marking the environment keeps the report to one, since the worker inherits it.
|
|
97
|
+
*/
|
|
98
|
+
/**
|
|
99
|
+
* The directory of the config that called this.
|
|
100
|
+
*
|
|
101
|
+
* A monolith loads each app's own config, so the working directory is the
|
|
102
|
+
* monolith's and would attribute its files to whichever app is being loaded.
|
|
103
|
+
*/
|
|
104
|
+
const callerDirectory = () => {
|
|
105
|
+
const original = Error.prepareStackTrace;
|
|
106
|
+
try {
|
|
107
|
+
Error.prepareStackTrace = (_, stack) => stack;
|
|
108
|
+
const stack = new Error().stack;
|
|
109
|
+
for (const frame of stack ?? []) {
|
|
110
|
+
const file = frame.getFileName?.();
|
|
111
|
+
if (!file || file.includes('node_modules') || !file.includes('next.config')) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
return path.dirname(file.replace(/^file:\/\//, ''));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
Error.prepareStackTrace = original;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
};
|
|
125
|
+
const REPORTED_ENV = '__FG_MONOLITH_PORTABILITY_REPORTED';
|
|
126
|
+
const alreadyReported = (root) => (process.env[REPORTED_ENV] ?? '').split(path.delimiter).includes(root);
|
|
127
|
+
const markReported = (root) => {
|
|
128
|
+
const seen = (process.env[REPORTED_ENV] ?? '').split(path.delimiter).filter(Boolean);
|
|
129
|
+
process.env[REPORTED_ENV] = [...seen, root].join(path.delimiter);
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Write a report once per build, however many processes load the config:
|
|
133
|
+
* each build worker inherits the environment this marks.
|
|
134
|
+
*/
|
|
135
|
+
export const reportOnce = (key, report) => {
|
|
136
|
+
if (alreadyReported(key))
|
|
137
|
+
return;
|
|
138
|
+
markReported(key);
|
|
139
|
+
process.stderr.write(`${report}\n`);
|
|
140
|
+
};
|
|
141
|
+
const describe = (problems) => [
|
|
142
|
+
`This app would not port cleanly into a monolith:`,
|
|
143
|
+
...problems.map((problem) => ` - ${problem}`),
|
|
144
|
+
`See @fairgarden/monolith for what a mounted app can rely on.`,
|
|
145
|
+
].join('\n');
|
|
146
|
+
/**
|
|
147
|
+
* Check that an app can be mounted into a monolith, from the app's own config.
|
|
148
|
+
*
|
|
149
|
+
* Wrap the app's config with this and it reports, on every build, whatever
|
|
150
|
+
* would not survive being served under a prefix alongside other apps. It
|
|
151
|
+
* changes nothing about how the app runs on its own.
|
|
152
|
+
*
|
|
153
|
+
* ```ts
|
|
154
|
+
* // apps/id/next.config.ts
|
|
155
|
+
* export default withMonolithicPortability({ redirects: async () => [...] })
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* Set `level: 'error'` in CI to make it a gate rather than a reminder.
|
|
159
|
+
*/
|
|
160
|
+
export const withMonolithicPortability = (nextConfig = {}, options = {}) => {
|
|
161
|
+
const root = options.root ?? callerDirectory() ?? process.cwd();
|
|
162
|
+
const ignore = new Set(options.ignore ?? []);
|
|
163
|
+
const problems = findPortabilityProblems(nextConfig, root).filter((problem) => !ignore.has(problem.slice(0, problem.indexOf(':'))));
|
|
164
|
+
if (problems.length > 0) {
|
|
165
|
+
if (options.level === 'error')
|
|
166
|
+
throw new Error(describe(problems));
|
|
167
|
+
reportOnce(root, describe(problems));
|
|
168
|
+
}
|
|
169
|
+
return nextConfig;
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=portability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portability.js","sourceRoot":"","sources":["../src/portability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import NextLink from 'next/link';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
export { createHref, mountPrefix, prefixHref, MOUNTS_ENV } from './mounts.ts';
|
|
4
|
+
type NextLinkProps = ComponentProps<typeof NextLink>;
|
|
5
|
+
/**
|
|
6
|
+
* A `next/link` that knows where its app is mounted.
|
|
7
|
+
*
|
|
8
|
+
* The monolith does not use `basePath`, so nothing prefixes an app's own hrefs
|
|
9
|
+
* for it. Build one of these per app and use it in place of `next/link`:
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* // lib/link.ts
|
|
13
|
+
* export const Link = createLink('@fairgarden/id')
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Standalone the prefix is empty and this is `next/link` with an extra call.
|
|
17
|
+
*/
|
|
18
|
+
export declare const createLink: (packageName: string) => {
|
|
19
|
+
({ href, ...props }: NextLinkProps): import("react").JSX.Element;
|
|
20
|
+
displayName: string;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=portable-link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portable-link.d.ts","sourceRoot":"","sources":["../src/portable-link.tsx"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,WAAW,CAAA;AAChC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAG3C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7E,KAAK,aAAa,GAAG,cAAc,CAAC,OAAO,QAAQ,CAAC,CAAA;AAEpD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,GAAI,aAAa,MAAM;yBAGF,aAAa;;CAKxD,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import NextLink from 'next/link';
|
|
3
|
+
import { mountPrefix, prefixHref } from "./mounts.js";
|
|
4
|
+
export { createHref, mountPrefix, prefixHref, MOUNTS_ENV } from "./mounts.js";
|
|
5
|
+
/**
|
|
6
|
+
* A `next/link` that knows where its app is mounted.
|
|
7
|
+
*
|
|
8
|
+
* The monolith does not use `basePath`, so nothing prefixes an app's own hrefs
|
|
9
|
+
* for it. Build one of these per app and use it in place of `next/link`:
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* // lib/link.ts
|
|
13
|
+
* export const Link = createLink('@fairgarden/id')
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Standalone the prefix is empty and this is `next/link` with an extra call.
|
|
17
|
+
*/
|
|
18
|
+
export const createLink = (packageName) => {
|
|
19
|
+
const prefix = mountPrefix(packageName);
|
|
20
|
+
const MonolithLink = ({ href, ...props }) => (_jsx(NextLink, { href: prefixHref(href, prefix), ...props }));
|
|
21
|
+
MonolithLink.displayName = `MonolithLink(${packageName})`;
|
|
22
|
+
return MonolithLink;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=portable-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portable-link.js","sourceRoot":"","sources":["../src/portable-link.tsx"],"names":[],"mappings":";AAAA,OAAO,QAAQ,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAErD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAI7E;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,WAAmB,EAAE,EAAE;IAChD,MAAM,MAAM,GAAG,WAAW,CAAC,WAAW,CAAC,CAAA;IAEvC,MAAM,YAAY,GAAG,CAAC,EAAE,IAAI,EAAE,GAAG,KAAK,EAAiB,EAAE,EAAE,CAAC,CAC1D,KAAC,QAAQ,IAAC,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,KAAM,KAAK,GAAI,CACxD,CAAA;IACD,YAAY,CAAC,WAAW,GAAG,gBAAgB,WAAW,GAAG,CAAA;IACzD,OAAO,YAAY,CAAA;AACrB,CAAC,CAAA"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ResolvedApp } from './types.ts';
|
|
2
|
+
/** A project's proxy, at its root or under `src/`, as Next looks for one. */
|
|
3
|
+
export declare const findProxy: (root: string) => string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Whether a proxy file is only the locale proxy: `proxy` (or `middleware`, or
|
|
6
|
+
* the default export) is a call to `createLocaleProxy`, and the only other
|
|
7
|
+
* export is `config`.
|
|
8
|
+
*
|
|
9
|
+
* Anything it cannot read that plainly — a wrapper around the call, a
|
|
10
|
+
* re-export, another export — counts as a proxy of the app's own, which is
|
|
11
|
+
* the safe answer: that one does need moving to the monolith.
|
|
12
|
+
*/
|
|
13
|
+
export declare const isLocaleProxy: (file: string) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The paths a proxy file's `config.matcher` names, read from the source as
|
|
16
|
+
* Next reads it. Undefined when there is no literal matcher to read, in which
|
|
17
|
+
* case the proxy runs for every path.
|
|
18
|
+
*/
|
|
19
|
+
export declare const proxyMatcher: (file: string) => string[] | undefined;
|
|
20
|
+
export interface UnservedProxy {
|
|
21
|
+
app: ResolvedApp;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Mounted apps with a locale proxy that the monolith's own proxy does not run
|
|
26
|
+
* at their mount, so nobody arriving at that app's root is offered their
|
|
27
|
+
* language.
|
|
28
|
+
*
|
|
29
|
+
* Only an app root counts: that is the one path the locale proxy acts on. A
|
|
30
|
+
* monolith proxy with no literal matcher runs everywhere and is taken on trust.
|
|
31
|
+
*/
|
|
32
|
+
export declare const unservedLocaleProxies: (monolithRoot: string, apps: ResolvedApp[]) => UnservedProxy[];
|
|
33
|
+
//# sourceMappingURL=proxies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxies.d.ts","sourceRoot":"","sources":["../src/proxies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAoC7C,6EAA6E;AAC7E,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,MAAM,GAAG,SAUjD,CAAA;AAcD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,OA6C5C,CAAA;AASD;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MAAM,EAAE,GAAG,SA0BtD,CAAA;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,WAAW,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAChC,cAAc,MAAM,EACpB,MAAM,WAAW,EAAE,KAClB,aAAa,EAyBf,CAAA"}
|
package/dist/proxies.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
const LOCALE_PROXY_MODULE = '@fairgarden/indicators/proxy';
|
|
5
|
+
const LOCALE_PROXY = 'createLocaleProxy';
|
|
6
|
+
/** What Next reads as a proxy, `middleware` being its older name. */
|
|
7
|
+
const PROXY_NAMES = ['proxy', 'middleware'];
|
|
8
|
+
const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs'];
|
|
9
|
+
const isFile = (candidate) => {
|
|
10
|
+
try {
|
|
11
|
+
return statSync(candidate).isFile();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** A project's proxy, at its root or under `src/`, as Next looks for one. */
|
|
18
|
+
export const findProxy = (root) => {
|
|
19
|
+
for (const dir of ['', 'src']) {
|
|
20
|
+
for (const name of PROXY_NAMES) {
|
|
21
|
+
for (const extension of EXTENSIONS) {
|
|
22
|
+
const candidate = path.join(root, dir, `${name}${extension}`);
|
|
23
|
+
if (isFile(candidate))
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
const statementsOf = (file) => {
|
|
31
|
+
try {
|
|
32
|
+
const ast = parse(readFileSync(file, 'utf8'), {
|
|
33
|
+
sourceType: 'module',
|
|
34
|
+
plugins: ['typescript', 'jsx'],
|
|
35
|
+
});
|
|
36
|
+
return ast.program.body;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Whether a proxy file is only the locale proxy: `proxy` (or `middleware`, or
|
|
44
|
+
* the default export) is a call to `createLocaleProxy`, and the only other
|
|
45
|
+
* export is `config`.
|
|
46
|
+
*
|
|
47
|
+
* Anything it cannot read that plainly — a wrapper around the call, a
|
|
48
|
+
* re-export, another export — counts as a proxy of the app's own, which is
|
|
49
|
+
* the safe answer: that one does need moving to the monolith.
|
|
50
|
+
*/
|
|
51
|
+
export const isLocaleProxy = (file) => {
|
|
52
|
+
const statements = statementsOf(file);
|
|
53
|
+
if (!statements)
|
|
54
|
+
return false;
|
|
55
|
+
// What the import is called here, which it may have been renamed to.
|
|
56
|
+
const names = new Set();
|
|
57
|
+
for (const statement of statements) {
|
|
58
|
+
if (statement.type !== 'ImportDeclaration')
|
|
59
|
+
continue;
|
|
60
|
+
if (statement.source.value !== LOCALE_PROXY_MODULE)
|
|
61
|
+
continue;
|
|
62
|
+
for (const specifier of statement.specifiers) {
|
|
63
|
+
if (specifier.type === 'ImportSpecifier' &&
|
|
64
|
+
specifier.imported.type === 'Identifier' &&
|
|
65
|
+
specifier.imported.name === LOCALE_PROXY) {
|
|
66
|
+
names.add(specifier.local.name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (names.size === 0)
|
|
71
|
+
return false;
|
|
72
|
+
const isCall = (node) => node?.type === 'CallExpression' &&
|
|
73
|
+
node.callee.type === 'Identifier' &&
|
|
74
|
+
names.has(node.callee.name);
|
|
75
|
+
let proxies = 0;
|
|
76
|
+
for (const statement of statements) {
|
|
77
|
+
if (statement.type === 'ExportAllDeclaration')
|
|
78
|
+
return false;
|
|
79
|
+
if (statement.type === 'ExportDefaultDeclaration') {
|
|
80
|
+
if (!isCall(statement.declaration))
|
|
81
|
+
return false;
|
|
82
|
+
proxies += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (statement.type !== 'ExportNamedDeclaration')
|
|
86
|
+
continue;
|
|
87
|
+
if (statement.specifiers.length > 0)
|
|
88
|
+
return false;
|
|
89
|
+
if (statement.declaration?.type !== 'VariableDeclaration')
|
|
90
|
+
return false;
|
|
91
|
+
for (const declarator of statement.declaration.declarations) {
|
|
92
|
+
if (declarator.id.type !== 'Identifier')
|
|
93
|
+
return false;
|
|
94
|
+
if (declarator.id.name === 'config')
|
|
95
|
+
continue;
|
|
96
|
+
if (!PROXY_NAMES.includes(declarator.id.name) || !isCall(declarator.init))
|
|
97
|
+
return false;
|
|
98
|
+
proxies += 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return proxies === 1;
|
|
102
|
+
};
|
|
103
|
+
const keyName = (node) => node.type === 'Identifier' && typeof node.name === 'string'
|
|
104
|
+
? node.name
|
|
105
|
+
: node.type === 'StringLiteral' && typeof node.value === 'string'
|
|
106
|
+
? node.value
|
|
107
|
+
: undefined;
|
|
108
|
+
/**
|
|
109
|
+
* The paths a proxy file's `config.matcher` names, read from the source as
|
|
110
|
+
* Next reads it. Undefined when there is no literal matcher to read, in which
|
|
111
|
+
* case the proxy runs for every path.
|
|
112
|
+
*/
|
|
113
|
+
export const proxyMatcher = (file) => {
|
|
114
|
+
for (const statement of statementsOf(file) ?? []) {
|
|
115
|
+
if (statement.type !== 'ExportNamedDeclaration')
|
|
116
|
+
continue;
|
|
117
|
+
if (statement.declaration?.type !== 'VariableDeclaration')
|
|
118
|
+
continue;
|
|
119
|
+
for (const declarator of statement.declaration.declarations) {
|
|
120
|
+
if (declarator.id.type !== 'Identifier' || declarator.id.name !== 'config')
|
|
121
|
+
continue;
|
|
122
|
+
if (declarator.init?.type !== 'ObjectExpression')
|
|
123
|
+
return undefined;
|
|
124
|
+
for (const property of declarator.init.properties) {
|
|
125
|
+
if (property.type !== 'ObjectProperty' || keyName(property.key) !== 'matcher')
|
|
126
|
+
continue;
|
|
127
|
+
const entries = property.value.type === 'ArrayExpression' ? property.value.elements : [property.value];
|
|
128
|
+
return entries.flatMap((entry) => {
|
|
129
|
+
if (entry?.type === 'StringLiteral')
|
|
130
|
+
return [entry.value];
|
|
131
|
+
if (entry?.type !== 'ObjectExpression')
|
|
132
|
+
return [];
|
|
133
|
+
const source = entry.properties.find((field) => field.type === 'ObjectProperty' && keyName(field.key) === 'source');
|
|
134
|
+
return source?.type === 'ObjectProperty' && source.value.type === 'StringLiteral'
|
|
135
|
+
? [source.value.value]
|
|
136
|
+
: [];
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Mounted apps with a locale proxy that the monolith's own proxy does not run
|
|
146
|
+
* at their mount, so nobody arriving at that app's root is offered their
|
|
147
|
+
* language.
|
|
148
|
+
*
|
|
149
|
+
* Only an app root counts: that is the one path the locale proxy acts on. A
|
|
150
|
+
* monolith proxy with no literal matcher runs everywhere and is taken on trust.
|
|
151
|
+
*/
|
|
152
|
+
export const unservedLocaleProxies = (monolithRoot, apps) => {
|
|
153
|
+
const withLocaleProxy = apps.flatMap((app) => {
|
|
154
|
+
const file = findProxy(app.root);
|
|
155
|
+
return file && isLocaleProxy(file) ? [{ app, file }] : [];
|
|
156
|
+
});
|
|
157
|
+
if (withLocaleProxy.length === 0)
|
|
158
|
+
return [];
|
|
159
|
+
const own = findProxy(monolithRoot);
|
|
160
|
+
const matcher = own ? proxyMatcher(own) : [];
|
|
161
|
+
if (matcher === undefined)
|
|
162
|
+
return [];
|
|
163
|
+
const where = own ? path.relative(monolithRoot, own) : 'proxy.ts';
|
|
164
|
+
return withLocaleProxy
|
|
165
|
+
.filter(({ app }) => !matcher.includes(app.prefix || '/'))
|
|
166
|
+
.map(({ app, file }) => {
|
|
167
|
+
const mount = app.prefix || '/';
|
|
168
|
+
return {
|
|
169
|
+
app,
|
|
170
|
+
message: `"${app.name}" negotiates its locale in ${path.basename(file)}, which a ` +
|
|
171
|
+
`monolith does not mount; its own ${where} has to do it at ${mount}. ` +
|
|
172
|
+
`Add the app to createLocaleProxy({ '${mount}': ... }) there, and '${mount}' ` +
|
|
173
|
+
'to its config.matcher.',
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=proxies.js.map
|