render-workflows-dart 0.8.2
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/CHANGELOG.md +193 -0
- package/LICENSE +21 -0
- package/README.md +678 -0
- package/dart/generator/bin/generate.dart +419 -0
- package/dart/generator/pubspec.lock +149 -0
- package/dart/generator/pubspec.yaml +9 -0
- package/examples/README.md +56 -0
- package/examples/default/README.md +18 -0
- package/examples/default/gitignore +7 -0
- package/examples/default/index.js +2 -0
- package/examples/default/package.json +17 -0
- package/examples/default/pubspec.yaml +7 -0
- package/examples/default/tasks.dart +39 -0
- package/examples/http/README.md +26 -0
- package/examples/http/gitignore +7 -0
- package/examples/http/index.js +2 -0
- package/examples/http/package.json +18 -0
- package/examples/http/pubspec.yaml +13 -0
- package/examples/http/tasks.dart +100 -0
- package/examples/introspect/README.md +50 -0
- package/examples/introspect/gitignore +7 -0
- package/examples/introspect/index.js +2 -0
- package/examples/introspect/node_env.dart +35 -0
- package/examples/introspect/package.json +18 -0
- package/examples/introspect/pubspec.yaml +23 -0
- package/examples/introspect/tasks.dart +144 -0
- package/examples/native/README.md +32 -0
- package/examples/native/gitignore +7 -0
- package/examples/native/index.js +2 -0
- package/examples/native/native/tools_impl.dart +105 -0
- package/examples/native/package.json +23 -0
- package/examples/native/pubspec.yaml +7 -0
- package/examples/native/tasks.dart +35 -0
- package/examples/postgres/README.md +73 -0
- package/examples/postgres/gitignore +7 -0
- package/examples/postgres/index.js +2 -0
- package/examples/postgres/native/db_impl.dart +205 -0
- package/examples/postgres/package.json +26 -0
- package/examples/postgres/pubspec.yaml +14 -0
- package/examples/postgres/seed/bin/seed.dart +56 -0
- package/examples/postgres/seed/bin/show.dart +64 -0
- package/examples/postgres/seed/lib/src/connect.dart +93 -0
- package/examples/postgres/seed/lib/src/schema.dart +46 -0
- package/examples/postgres/seed/pubspec.yaml +17 -0
- package/examples/postgres/tasks.dart +66 -0
- package/package.json +51 -0
- package/runtime/AGENTS.md +141 -0
- package/runtime/CLAUDE.md +2 -0
- package/runtime/native_task.dart +115 -0
- package/runtime/render_dart.dart +428 -0
- package/src/cli.js +396 -0
- package/src/native-worker.js +196 -0
- package/src/node-bridge.js +118 -0
- package/src/runtime.js +108 -0
- package/src/toolchain/compile.js +153 -0
- package/src/toolchain/dart-sdk.js +216 -0
- package/src/toolchain/dart-version.js +113 -0
- package/src/toolchain/generate.js +112 -0
- package/src/toolchain/index.js +8 -0
- package/src/toolchain/native.js +217 -0
- package/src/web-shims.js +281 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Deciding *which* Dart to use, separately from finding or fetching one.
|
|
2
|
+
//
|
|
3
|
+
// Kept apart from dart-sdk.js because the questions are different: this file
|
|
4
|
+
// answers "what did the user ask for", that one answers "where is it". Mixing
|
|
5
|
+
// them is how the pin came to be consulted only on the download path.
|
|
6
|
+
|
|
7
|
+
const CHANNELS = ['stable', 'beta', 'dev'];
|
|
8
|
+
|
|
9
|
+
/** The version this package installs when nobody has asked for one. */
|
|
10
|
+
const DEFAULT_DART_VERSION = '3.13.1';
|
|
11
|
+
|
|
12
|
+
const ARCHIVE = 'https://storage.googleapis.com/dart-archive/channels';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What the user asked for, and how firmly.
|
|
16
|
+
*
|
|
17
|
+
* `explicit` is the important part. A pin someone typed has to be honoured even
|
|
18
|
+
* when a different Dart is already on PATH, or it is not a pin. The built-in
|
|
19
|
+
* default has no such claim: it exists so a first build on Render has something
|
|
20
|
+
* to fetch, and deferring to a local toolchain there is a courtesy, not a bug.
|
|
21
|
+
*
|
|
22
|
+
* Precedence, highest first: the command line, the environment, package.json.
|
|
23
|
+
* The flag is for trying something once, the environment for varying a build
|
|
24
|
+
* without a commit — Render's dashboard sets those — and package.json for the
|
|
25
|
+
* answer that should travel with the project.
|
|
26
|
+
*/
|
|
27
|
+
function requestedVersion({ flag, env, config }) {
|
|
28
|
+
if (flag) return { version: flag, explicit: true, from: '--dart-version' };
|
|
29
|
+
if (env) return { version: env, explicit: true, from: 'RENDER_DART_VERSION' };
|
|
30
|
+
if (config) return { version: config, explicit: true, from: 'package.json' };
|
|
31
|
+
return { version: DEFAULT_DART_VERSION, explicit: false, from: 'default' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Whether a request names a moving target rather than one release. */
|
|
35
|
+
function isAlias(version) {
|
|
36
|
+
return version === 'latest' || CHANNELS.includes(version);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Turns `latest` or a channel name into the version it currently means.
|
|
41
|
+
*
|
|
42
|
+
* An exact version resolves to itself without touching the network, so a
|
|
43
|
+
* pinned project keeps building when the archive is unreachable — which is the
|
|
44
|
+
* main practical argument for pinning one.
|
|
45
|
+
*/
|
|
46
|
+
async function resolveVersion(version, { log = () => {} } = {}) {
|
|
47
|
+
if (!isAlias(version)) return version;
|
|
48
|
+
|
|
49
|
+
const channel = version === 'latest' ? 'stable' : version;
|
|
50
|
+
const url = `${ARCHIVE}/${channel}/release/latest/VERSION`;
|
|
51
|
+
const res = await fetch(url);
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Could not resolve Dart "${version}": ${res.status} from ${url}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const { version: resolved } = await res.json();
|
|
58
|
+
log(`"${version}" is Dart ${resolved} today`);
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every version the archive offers on a channel, newest first.
|
|
64
|
+
*
|
|
65
|
+
* The bucket lists build-number directories alongside version ones — 28 against
|
|
66
|
+
* 176 on stable — so anything without a dot is dropped.
|
|
67
|
+
*/
|
|
68
|
+
async function listVersions(channel = 'stable') {
|
|
69
|
+
if (!CHANNELS.includes(channel)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Unknown channel "${channel}". Choose one of: ${CHANNELS.join(', ')}.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const url =
|
|
75
|
+
'https://storage.googleapis.com/storage/v1/b/dart-archive/o' +
|
|
76
|
+
`?delimiter=/&prefix=channels/${channel}/release/&fields=prefixes`;
|
|
77
|
+
|
|
78
|
+
const res = await fetch(url);
|
|
79
|
+
if (!res.ok) throw new Error(`Could not list Dart versions: ${res.status}`);
|
|
80
|
+
const { prefixes = [] } = await res.json();
|
|
81
|
+
|
|
82
|
+
return prefixes
|
|
83
|
+
.map((p) => p.split('/').filter(Boolean).pop())
|
|
84
|
+
.filter((name) => name.includes('.'))
|
|
85
|
+
.sort(compareVersions)
|
|
86
|
+
.reverse();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Numeric where both are numeric, so 3.9.0 sorts below 3.10.0. */
|
|
90
|
+
function compareVersions(a, b) {
|
|
91
|
+
const parts = (v) => v.split(/[.\-+]/).map((n) => (/^\d+$/.test(n) ? +n : n));
|
|
92
|
+
const [x, y] = [parts(a), parts(b)];
|
|
93
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
94
|
+
const [p, q] = [x[i], y[i]];
|
|
95
|
+
if (p === q) continue;
|
|
96
|
+
if (p === undefined) return -1;
|
|
97
|
+
if (q === undefined) return 1;
|
|
98
|
+
if (typeof p === typeof q) return p < q ? -1 : 1;
|
|
99
|
+
// A pre-release suffix sorts below the plain release it belongs to.
|
|
100
|
+
return typeof p === 'number' ? 1 : -1;
|
|
101
|
+
}
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
CHANNELS,
|
|
107
|
+
DEFAULT_DART_VERSION,
|
|
108
|
+
requestedVersion,
|
|
109
|
+
isAlias,
|
|
110
|
+
resolveVersion,
|
|
111
|
+
listVersions,
|
|
112
|
+
compareVersions,
|
|
113
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Driving the Dart generator that turns @nativeTask functions into a
|
|
2
|
+
// dispatcher and typed stubs.
|
|
3
|
+
//
|
|
4
|
+
// The generator is a Dart program with its own pubspec, shipped inside this
|
|
5
|
+
// package and resolved into the project's node_modules pub cache. Keeping it
|
|
6
|
+
// self-contained means depending on package:analyzer never touches the user's
|
|
7
|
+
// own pubspec.
|
|
8
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
9
|
+
const { existsSync, readFileSync, writeFileSync, mkdirSync } = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
const GENERATOR_DIR = path.join(__dirname, '..', '..', 'dart', 'generator');
|
|
13
|
+
|
|
14
|
+
/** Resolves the generator's own dependencies, once. */
|
|
15
|
+
function ensureGenerator(dart, pubCache, log) {
|
|
16
|
+
const marker = path.join(GENERATOR_DIR, '.dart_tool', 'package_config.json');
|
|
17
|
+
if (existsSync(marker)) return;
|
|
18
|
+
|
|
19
|
+
log('resolving generator dependencies (first native build)');
|
|
20
|
+
const result = spawnSync(dart, ['pub', 'get'], {
|
|
21
|
+
cwd: GENERATOR_DIR,
|
|
22
|
+
stdio: 'pipe',
|
|
23
|
+
env: { ...process.env, PUB_CACHE: pubCache ?? process.env.PUB_CACHE },
|
|
24
|
+
});
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`could not resolve the render-dart generator:\n${(result.stderr ?? '').toString().trim()}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Makes sure a runtime file the generated code imports is present.
|
|
34
|
+
*
|
|
35
|
+
* Written when missing. Never overwritten — an author may have edited it — but
|
|
36
|
+
* a difference is reported, because a stale copy alongside a newer render-dart
|
|
37
|
+
* is exactly how the `^0.1.0` template pin went unnoticed for three releases.
|
|
38
|
+
*/
|
|
39
|
+
function ensureRuntimeFile(root, name, log) {
|
|
40
|
+
const shipped = path.join(__dirname, '..', '..', 'runtime', name);
|
|
41
|
+
const local = path.join(root, name);
|
|
42
|
+
|
|
43
|
+
if (!existsSync(local)) {
|
|
44
|
+
writeFileSync(local, readFileSync(shipped));
|
|
45
|
+
log(`added ${name}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (readFileSync(local, 'utf8') !== readFileSync(shipped, 'utf8')) {
|
|
49
|
+
// Worth being loud about: the failure mode is a compile error against a
|
|
50
|
+
// signature that changed, which reads as a bug in the generated code
|
|
51
|
+
// rather than as a stale file.
|
|
52
|
+
log(
|
|
53
|
+
`note: ${name} differs from the copy shipped with this render-dart. ` +
|
|
54
|
+
`If the build fails on a signature it does not recognise, refresh it: ` +
|
|
55
|
+
`cp node_modules/render-workflows-dart/runtime/${name} ${name}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generates the dispatcher and stubs for one native task entry.
|
|
62
|
+
*
|
|
63
|
+
* Returns the dispatcher path, which is what actually gets compiled — the
|
|
64
|
+
* author's file is imported by it rather than compiled directly.
|
|
65
|
+
*/
|
|
66
|
+
function generate({ dart, root, entry, pubCache, log }) {
|
|
67
|
+
ensureGenerator(dart, pubCache, log);
|
|
68
|
+
ensureRuntimeFile(root, 'native_task.dart', log);
|
|
69
|
+
|
|
70
|
+
const main = path.join(root, '.dart_tool', 'render_dart', `${entry.name}.main.dart`);
|
|
71
|
+
mkdirSync(path.dirname(main), { recursive: true });
|
|
72
|
+
|
|
73
|
+
// Only pass an override when package.json actually set one; otherwise the
|
|
74
|
+
// annotation decides.
|
|
75
|
+
const overrides = [];
|
|
76
|
+
if (entry.worker !== undefined) overrides.push('--worker', String(entry.worker));
|
|
77
|
+
if (entry.idleTimeoutMs !== undefined) overrides.push('--idle', String(entry.idleTimeoutMs));
|
|
78
|
+
if (entry.timeoutMs !== undefined) overrides.push('--timeout', String(entry.timeoutMs));
|
|
79
|
+
|
|
80
|
+
const result = spawnSync(
|
|
81
|
+
dart,
|
|
82
|
+
[
|
|
83
|
+
'run',
|
|
84
|
+
path.join('bin', 'generate.dart'),
|
|
85
|
+
'--project', root,
|
|
86
|
+
'--entry', entry.entry,
|
|
87
|
+
'--name', entry.name,
|
|
88
|
+
'--stub', entry.stub,
|
|
89
|
+
'--facade', entry.facade,
|
|
90
|
+
'--main', main,
|
|
91
|
+
...overrides,
|
|
92
|
+
],
|
|
93
|
+
{
|
|
94
|
+
cwd: GENERATOR_DIR,
|
|
95
|
+
stdio: 'pipe',
|
|
96
|
+
env: { ...process.env, PUB_CACHE: pubCache ?? process.env.PUB_CACHE },
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (result.status !== 0) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`native task ${entry.rel} could not be generated:\n` +
|
|
103
|
+
`${(result.stderr ?? '').toString().trim()}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const { methods } = JSON.parse((result.stdout ?? '{}').toString().trim() || '{}');
|
|
108
|
+
log(`native ${entry.name}: ${(methods ?? []).length} task(s) — ${(methods ?? []).join(', ')}`);
|
|
109
|
+
return main;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { generate, ensureRuntimeFile, GENERATOR_DIR };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The Dart -> JavaScript toolchain, free of Render specifics.
|
|
2
|
+
//
|
|
3
|
+
// Kept separate so it can be extracted into a general Dart/Node bridge if one
|
|
4
|
+
// is ever wanted; nothing in here knows what a workflow is.
|
|
5
|
+
module.exports = {
|
|
6
|
+
...require('./dart-sdk'),
|
|
7
|
+
...require('./compile'),
|
|
8
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Compiling declared Dart files to native executables.
|
|
2
|
+
//
|
|
3
|
+
// Render's own container can do this: the SDK render-dart vendors is the full
|
|
4
|
+
// SDK, carrying gen_snapshot and dartaotruntime, and the build host is already
|
|
5
|
+
// linux/x64. So nothing is cross-compiled and no binary is committed — the
|
|
6
|
+
// executable is produced from the source in the commit that deploys it.
|
|
7
|
+
const { execFileSync } = require('node:child_process');
|
|
8
|
+
const { createHash } = require('node:crypto');
|
|
9
|
+
const { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync, chmodSync } = require('node:fs');
|
|
10
|
+
const { readdir } = require('node:fs/promises');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
|
|
13
|
+
const CACHE_DIR = path.join('node_modules', '.native-cache');
|
|
14
|
+
const OUT_DIR = path.join('build', 'native');
|
|
15
|
+
const SKIP_DIRS = new Set(['build', 'node_modules', '.dart_tool', '.git']);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalises `renderDart.native` into full entries.
|
|
19
|
+
*
|
|
20
|
+
* Accepts a bare path as shorthand for a wrapped native task, or an object for
|
|
21
|
+
* anything else:
|
|
22
|
+
*
|
|
23
|
+
* "native": [
|
|
24
|
+
* "native/image_tools.dart",
|
|
25
|
+
* { "entry": "native/raw.dart", "mode": "exe" },
|
|
26
|
+
* { "entry": "native/hot.dart", "worker": true, "idleTimeoutMs": 30000 }
|
|
27
|
+
* ]
|
|
28
|
+
*/
|
|
29
|
+
function nativeEntries(root, declared) {
|
|
30
|
+
const seen = new Map();
|
|
31
|
+
|
|
32
|
+
return (declared ?? []).map((raw) => {
|
|
33
|
+
const spec = typeof raw === 'string' ? { entry: raw } : { ...raw };
|
|
34
|
+
if (!spec.entry) {
|
|
35
|
+
throw new Error('every renderDart.native entry needs an "entry" path');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mode = spec.mode ?? 'task';
|
|
39
|
+
if (mode !== 'task' && mode !== 'exe') {
|
|
40
|
+
throw new Error(`unknown native mode "${mode}" for ${spec.entry} (expected "task" or "exe")`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Left undefined when package.json says nothing, so the annotation on the
|
|
44
|
+
// function decides. Only an explicit value here overrides it.
|
|
45
|
+
const worker = spec.worker;
|
|
46
|
+
// An "exe" owns its own main and has no dispatch loop to keep alive, so
|
|
47
|
+
// there is nothing for a worker to talk to.
|
|
48
|
+
if (worker === true && mode !== 'task') {
|
|
49
|
+
throw new Error(`"worker" needs mode "task", but ${spec.entry} is mode "exe"`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entry = path.resolve(root, spec.entry);
|
|
53
|
+
// `native/probe_impl.dart` is the implementation; the generated facade
|
|
54
|
+
// takes the plain name, so callers write `import 'native/probe.dart'` and
|
|
55
|
+
// never see a generated filename.
|
|
56
|
+
const base = path.basename(spec.entry, '.dart');
|
|
57
|
+
const name = base.endsWith('_impl') ? base.slice(0, -'_impl'.length) : base;
|
|
58
|
+
|
|
59
|
+
const dir = path.dirname(entry);
|
|
60
|
+
const facade = path.join(dir, `${name}.dart`);
|
|
61
|
+
if (facade === entry) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`${spec.entry} would be overwritten by its own generated facade. ` +
|
|
64
|
+
`Name the implementation ${name}_impl.dart, so the facade can take ` +
|
|
65
|
+
`${name}.dart and callers import the plain name.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (seen.has(name)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`two native entries are both named "${name}" (${seen.get(name)} and ${spec.entry}); ` +
|
|
72
|
+
`they would compile to the same executable`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
seen.set(name, spec.entry);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
entry,
|
|
80
|
+
rel: spec.entry,
|
|
81
|
+
dir,
|
|
82
|
+
facade,
|
|
83
|
+
stub: path.join(dir, `${name}.stub.dart`),
|
|
84
|
+
mode,
|
|
85
|
+
worker,
|
|
86
|
+
idleTimeoutMs: spec.idleTimeoutMs,
|
|
87
|
+
timeoutMs: spec.timeoutMs,
|
|
88
|
+
out: path.join(root, OUT_DIR, name),
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Every .dart file under `dir`, sorted, so a fingerprint is stable. */
|
|
94
|
+
async function dartFilesUnder(dir) {
|
|
95
|
+
const found = [];
|
|
96
|
+
async function walk(d) {
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const e of entries) {
|
|
104
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.')) continue;
|
|
105
|
+
const full = path.join(d, e.name);
|
|
106
|
+
if (e.isDirectory()) await walk(full);
|
|
107
|
+
else if (e.name.endsWith('.dart')) found.push(full);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await walk(dir);
|
|
111
|
+
return found.sort();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Content fingerprint of everything that feeds one native executable.
|
|
116
|
+
*
|
|
117
|
+
* Keyed on content, never mtime. Every Render deploy is a fresh git checkout,
|
|
118
|
+
* which stamps the current time on every file — so an mtime comparison can
|
|
119
|
+
* never hit, and the cache silently does nothing. That was measured against
|
|
120
|
+
* Render's build log before this was written.
|
|
121
|
+
*/
|
|
122
|
+
async function fingerprint(root, entry) {
|
|
123
|
+
const files = await dartFilesUnder(entry.dir);
|
|
124
|
+
const h = createHash('sha256');
|
|
125
|
+
h.update(entry.mode).update('\0').update(String(entry.worker)).update('\0');
|
|
126
|
+
for (const f of files) {
|
|
127
|
+
h.update(path.relative(root, f)).update('\0').update(readFileSync(f));
|
|
128
|
+
}
|
|
129
|
+
for (const meta of ['pubspec.yaml', 'pubspec.lock']) {
|
|
130
|
+
const p = path.join(root, meta);
|
|
131
|
+
if (existsSync(p)) h.update(meta).update('\0').update(readFileSync(p));
|
|
132
|
+
}
|
|
133
|
+
return h.digest('hex');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Compiles each entry, reusing a cached binary when its sources are unchanged.
|
|
138
|
+
*
|
|
139
|
+
* Output lands in node_modules/.native-cache first and is copied to
|
|
140
|
+
* build/native/. That is deliberate: Render's build cache preserves
|
|
141
|
+
* node_modules and nothing else, so a deploy touching only tasks.dart reuses
|
|
142
|
+
* the executable instead of paying for another AOT compile.
|
|
143
|
+
*/
|
|
144
|
+
async function buildNative({ dart, root, entries, pubCache, log }) {
|
|
145
|
+
const cacheDir = path.join(root, CACHE_DIR);
|
|
146
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
147
|
+
mkdirSync(path.join(root, OUT_DIR), { recursive: true });
|
|
148
|
+
|
|
149
|
+
const env = pubCache ? { ...process.env, PUB_CACHE: pubCache } : process.env;
|
|
150
|
+
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
// A "task" entry compiles the generated dispatcher, which imports the
|
|
153
|
+
// author's file; an "exe" entry compiles the file itself.
|
|
154
|
+
const source = entry.mode === 'task' ? entry.main : entry.entry;
|
|
155
|
+
const cached = path.join(cacheDir, entry.name);
|
|
156
|
+
const stamp = `${cached}.sha256`;
|
|
157
|
+
const want = await fingerprint(root, entry);
|
|
158
|
+
|
|
159
|
+
const hit =
|
|
160
|
+
existsSync(cached) &&
|
|
161
|
+
existsSync(stamp) &&
|
|
162
|
+
readFileSync(stamp, 'utf8').trim() === want;
|
|
163
|
+
|
|
164
|
+
if (hit) {
|
|
165
|
+
log(`native ${entry.name}: cache hit (${want.slice(0, 12)})`);
|
|
166
|
+
} else {
|
|
167
|
+
log(`native ${entry.name}: compiling ${path.relative(root, source)}`);
|
|
168
|
+
execFileSync(dart, ['compile', 'exe', source, '-o', cached], {
|
|
169
|
+
cwd: root,
|
|
170
|
+
stdio: 'inherit',
|
|
171
|
+
env,
|
|
172
|
+
});
|
|
173
|
+
writeFileSync(stamp, want);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
copyFileSync(cached, entry.out);
|
|
177
|
+
chmodSync(entry.out, 0o755);
|
|
178
|
+
const { size } = statSync(entry.out);
|
|
179
|
+
log(`native ${entry.name}: ${path.relative(root, entry.out)} (${(size / 1e6).toFixed(1)} MB)`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Writes a .gitignore listing what the build generated in each native dir.
|
|
185
|
+
*
|
|
186
|
+
* The facade takes a natural name like `probe.dart`, so without this it reads
|
|
187
|
+
* as hand-written source. Having the build maintain the list keeps it right as
|
|
188
|
+
* entries are added or renamed.
|
|
189
|
+
*/
|
|
190
|
+
function writeGeneratedIgnores(root, entries) {
|
|
191
|
+
const byDir = new Map();
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (entry.mode !== 'task') continue;
|
|
194
|
+
const names = byDir.get(entry.dir) ?? [];
|
|
195
|
+
names.push(path.basename(entry.facade), path.basename(entry.stub));
|
|
196
|
+
byDir.set(entry.dir, names);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const [dir, names] of byDir) {
|
|
200
|
+
writeFileSync(
|
|
201
|
+
path.join(dir, '.gitignore'),
|
|
202
|
+
'# Written by `render-dart build`. These are generated from the\n' +
|
|
203
|
+
'# @nativeTask sources beside them and would only ever go stale in git.\n' +
|
|
204
|
+
`${names.sort().join('\n')}\n`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
nativeEntries,
|
|
211
|
+
buildNative,
|
|
212
|
+
fingerprint,
|
|
213
|
+
dartFilesUnder,
|
|
214
|
+
writeGeneratedIgnores,
|
|
215
|
+
CACHE_DIR,
|
|
216
|
+
OUT_DIR,
|
|
217
|
+
};
|