@johnhenry/sitefile 0.0.0-stage → 0.0.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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +475 -2
- package/lib/src/archive-writers.d.ts +31 -0
- package/lib/src/archive-writers.js +197 -0
- package/lib/src/attest.d.ts +126 -0
- package/lib/src/attest.js +220 -0
- package/lib/src/bake.d.ts +74 -0
- package/lib/src/bake.js +372 -0
- package/lib/src/base-image.d.ts +76 -0
- package/lib/src/base-image.js +138 -0
- package/lib/src/build-args.d.ts +26 -0
- package/lib/src/build-args.js +164 -0
- package/lib/src/cache-limit.d.ts +54 -0
- package/lib/src/cache-limit.js +239 -0
- package/lib/src/casefold-data.d.ts +2 -0
- package/lib/src/casefold-data.js +217 -0
- package/lib/src/cbor.d.ts +39 -0
- package/lib/src/cbor.js +174 -0
- package/lib/src/checklinks.d.ts +42 -0
- package/lib/src/checklinks.js +118 -0
- package/lib/src/cli/bake.d.ts +2 -0
- package/lib/src/cli/bake.js +179 -0
- package/lib/src/cli/build-image.d.ts +21 -0
- package/lib/src/cli/build-image.js +42 -0
- package/lib/src/cli/cache.d.ts +8 -0
- package/lib/src/cli/cache.js +109 -0
- package/lib/src/cli/common.d.ts +29 -0
- package/lib/src/cli/common.js +100 -0
- package/lib/src/cli/git-context.d.ts +18 -0
- package/lib/src/cli/git-context.js +48 -0
- package/lib/src/cli/images.d.ts +6 -0
- package/lib/src/cli/images.js +285 -0
- package/lib/src/cli/index.d.ts +10 -0
- package/lib/src/cli/index.js +95 -0
- package/lib/src/cli/outdated.d.ts +5 -0
- package/lib/src/cli/outdated.js +94 -0
- package/lib/src/cli/registry.d.ts +2 -0
- package/lib/src/cli/registry.js +122 -0
- package/lib/src/cli/serve-worker.d.ts +2 -0
- package/lib/src/cli/serve-worker.js +42 -0
- package/lib/src/cli/servers.d.ts +15 -0
- package/lib/src/cli/servers.js +451 -0
- package/lib/src/cli/vendor.d.ts +2 -0
- package/lib/src/cli/vendor.js +92 -0
- package/lib/src/cli/watch.d.ts +26 -0
- package/lib/src/cli/watch.js +81 -0
- package/lib/src/cli.d.ts +12 -0
- package/lib/src/cli.js +486 -0
- package/lib/src/csv.d.ts +27 -0
- package/lib/src/csv.js +117 -0
- package/lib/src/deflate.d.ts +76 -0
- package/lib/src/deflate.js +604 -0
- package/lib/src/digest.d.ts +3 -0
- package/lib/src/digest.js +7 -0
- package/lib/src/errors.d.ts +34 -0
- package/lib/src/errors.js +20 -0
- package/lib/src/executor.d.ts +386 -0
- package/lib/src/executor.js +2193 -0
- package/lib/src/explain.d.ts +64 -0
- package/lib/src/explain.js +445 -0
- package/lib/src/export-formats.d.ts +16 -0
- package/lib/src/export-formats.js +52 -0
- package/lib/src/exporter.d.ts +103 -0
- package/lib/src/exporter.js +676 -0
- package/lib/src/fingerprint.d.ts +40 -0
- package/lib/src/fingerprint.js +168 -0
- package/lib/src/frontmatter.d.ts +22 -0
- package/lib/src/frontmatter.js +45 -0
- package/lib/src/index.d.ts +12 -0
- package/lib/src/index.js +12 -0
- package/lib/src/language/ast.d.ts +353 -0
- package/lib/src/language/ast.js +57 -0
- package/lib/src/language/parser.d.ts +3 -0
- package/lib/src/language/parser.js +612 -0
- package/lib/src/language/scanner.d.ts +110 -0
- package/lib/src/language/scanner.js +449 -0
- package/lib/src/language/validate.d.ts +13 -0
- package/lib/src/language/validate.js +367 -0
- package/lib/src/links.d.ts +79 -0
- package/lib/src/links.js +583 -0
- package/lib/src/lint.d.ts +10 -0
- package/lib/src/lint.js +223 -0
- package/lib/src/markdown.d.ts +29 -0
- package/lib/src/markdown.js +612 -0
- package/lib/src/media.d.ts +82 -0
- package/lib/src/media.js +650 -0
- package/lib/src/memo.d.ts +57 -0
- package/lib/src/memo.js +93 -0
- package/lib/src/oci.d.ts +178 -0
- package/lib/src/oci.js +380 -0
- package/lib/src/outdated.d.ts +46 -0
- package/lib/src/outdated.js +195 -0
- package/lib/src/pages.d.ts +10 -0
- package/lib/src/pages.js +54 -0
- package/lib/src/paths.d.ts +39 -0
- package/lib/src/paths.js +101 -0
- package/lib/src/planner.d.ts +297 -0
- package/lib/src/planner.js +410 -0
- package/lib/src/plugins/cache.d.ts +70 -0
- package/lib/src/plugins/cache.js +226 -0
- package/lib/src/plugins/exec.d.ts +51 -0
- package/lib/src/plugins/exec.js +144 -0
- package/lib/src/plugins/host.d.ts +75 -0
- package/lib/src/plugins/host.js +277 -0
- package/lib/src/plugins/wasm.d.ts +23 -0
- package/lib/src/plugins/wasm.js +249 -0
- package/lib/src/profile.d.ts +42 -0
- package/lib/src/profile.js +68 -0
- package/lib/src/ref.d.ts +40 -0
- package/lib/src/ref.js +79 -0
- package/lib/src/registry-auth.d.ts +32 -0
- package/lib/src/registry-auth.js +151 -0
- package/lib/src/registry-cache.d.ts +71 -0
- package/lib/src/registry-cache.js +333 -0
- package/lib/src/registry-ops.d.ts +51 -0
- package/lib/src/registry-ops.js +248 -0
- package/lib/src/registry.d.ts +109 -0
- package/lib/src/registry.js +505 -0
- package/lib/src/render.d.ts +134 -0
- package/lib/src/render.js +576 -0
- package/lib/src/resources/archive.d.ts +11 -0
- package/lib/src/resources/archive.js +306 -0
- package/lib/src/resources/bundle-data.d.ts +16 -0
- package/lib/src/resources/bundle-data.js +50 -0
- package/lib/src/resources/convert.d.ts +12 -0
- package/lib/src/resources/convert.js +63 -0
- package/lib/src/resources/documents.d.ts +35 -0
- package/lib/src/resources/documents.js +122 -0
- package/lib/src/resources/git.d.ts +103 -0
- package/lib/src/resources/git.js +720 -0
- package/lib/src/resources/https.d.ts +40 -0
- package/lib/src/resources/https.js +256 -0
- package/lib/src/resources/ignore.d.ts +33 -0
- package/lib/src/resources/ignore.js +119 -0
- package/lib/src/resources/local.d.ts +64 -0
- package/lib/src/resources/local.js +248 -0
- package/lib/src/resources/lockfile.d.ts +52 -0
- package/lib/src/resources/lockfile.js +172 -0
- package/lib/src/resources/netpolicy.d.ts +6 -0
- package/lib/src/resources/netpolicy.js +82 -0
- package/lib/src/resources/types.d.ts +63 -0
- package/lib/src/resources/types.js +13 -0
- package/lib/src/resources/vendor.d.ts +94 -0
- package/lib/src/resources/vendor.js +296 -0
- package/lib/src/resources/wbn-reader.d.ts +44 -0
- package/lib/src/resources/wbn-reader.js +302 -0
- package/lib/src/schema.d.ts +25 -0
- package/lib/src/schema.js +169 -0
- package/lib/src/secrets.d.ts +21 -0
- package/lib/src/secrets.js +103 -0
- package/lib/src/serve-rules.d.ts +29 -0
- package/lib/src/serve-rules.js +122 -0
- package/lib/src/server.d.ts +45 -0
- package/lib/src/server.js +246 -0
- package/lib/src/servers.d.ts +71 -0
- package/lib/src/servers.js +139 -0
- package/lib/src/site.d.ts +60 -0
- package/lib/src/site.js +169 -0
- package/lib/src/store.d.ts +151 -0
- package/lib/src/store.js +596 -0
- package/lib/src/tree.d.ts +50 -0
- package/lib/src/tree.js +134 -0
- package/lib/src/values.d.ts +7 -0
- package/lib/src/values.js +37 -0
- package/lib/src/vendor.d.ts +19 -0
- package/lib/src/vendor.js +145 -0
- package/lib/src/watch.d.ts +44 -0
- package/lib/src/watch.js +137 -0
- package/lib/src/wbn.d.ts +5 -0
- package/lib/src/wbn.js +147 -0
- package/lib/src/yaml.d.ts +34 -0
- package/lib/src/yaml.js +562 -0
- package/package.json +58 -4
package/lib/src/cli.js
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { SitefileError, isSitefileError } from './errors.js';
|
|
5
|
+
import { build, buildToDirectory, checkPlanPins, lintBuild, planBuild } from './executor.js';
|
|
6
|
+
import { LINT_RULES } from './lint.js';
|
|
7
|
+
import { defaultOutName, formatFromExtension } from './export-formats.js';
|
|
8
|
+
import { collectBuildArgs } from './build-args.js';
|
|
9
|
+
import { insecureRegistriesFromEnv, makeClient } from './base-image.js';
|
|
10
|
+
import { cacheLimitFromEnv } from './cache-limit.js';
|
|
11
|
+
import { parseCacheRef, RegistryCache } from './registry-cache.js';
|
|
12
|
+
import { parseLabels } from './oci.js';
|
|
13
|
+
import { EXPORT_FORMATS, parseExportFormat, parseOciCompression } from './language/ast.js';
|
|
14
|
+
import { DEFAULT_FETCH_POLICY } from './resources/types.js';
|
|
15
|
+
import { parseLocalRef, refKey } from './ref.js';
|
|
16
|
+
import { EXTRA_HELP, isExtraCommand, runExtraCommand } from './cli/index.js';
|
|
17
|
+
import { evictAfterBuild, transformCacheDir } from './cli/cache.js';
|
|
18
|
+
import { storeBuildAsImage, validateImageTags } from './cli/build-image.js';
|
|
19
|
+
import { SITEFILE_VERSION } from './attest.js';
|
|
20
|
+
import { watchLoop } from './cli/watch.js';
|
|
21
|
+
import { Profiler } from './profile.js';
|
|
22
|
+
import { watchMemo } from './memo.js';
|
|
23
|
+
import { promises as fs } from 'node:fs';
|
|
24
|
+
import { graphDot, graphText, planGraph, why, whyText } from './explain.js';
|
|
25
|
+
import { isGitContext, materializeGitContext } from './cli/git-context.js';
|
|
26
|
+
import { loadSecrets } from './secrets.js';
|
|
27
|
+
const VERSION = SITEFILE_VERSION;
|
|
28
|
+
const HELP = `sitefile ${VERSION} — Docker-inspired static artifact builder
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
sitefile build [context] [options] build and export to --out (default: <context>/dist, or dist.<ext>)
|
|
32
|
+
sitefile validate [context] [options] parse, validate and evaluate variables (no IO)
|
|
33
|
+
sitefile inspect [context] [options] build in memory and list the exported files
|
|
34
|
+
sitefile graph [context] [options] print the dependency graph of imports, stages and operations
|
|
35
|
+
(text, --json or --dot; no IO, like validate)
|
|
36
|
+
sitefile why <path> [context] [opts] build in memory and show which instructions wrote an exported
|
|
37
|
+
file, from what inputs, and what overwrote it
|
|
38
|
+
The context may be a Git URL, https://host/repo.git#ref:dir (fetched to a temporary directory;
|
|
39
|
+
the default --out is then ./dist; see docs/CLI.md).
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
-f, --file <path> Sitefile path (default: <context>/Sitefile)
|
|
43
|
+
--target <stage> stop after the named stage and export its whole tree instead of
|
|
44
|
+
the EXPORT path (EXPORT's format still applies; no _redirects/_headers)
|
|
45
|
+
-o, --out <path> export destination (build only): a directory for 'directory',
|
|
46
|
+
a file for zip, tar, tgz, wbn
|
|
47
|
+
-t, --tag <name[:tag]> also store the build as a local image (build only, repeatable);
|
|
48
|
+
with -t and no -o nothing is written to the context
|
|
49
|
+
--format <fmt> directory | zip | tar | tgz (alias tar.gz) | wbn | oci (build only).
|
|
50
|
+
oci writes an OCI archive for -o *.tar (e.g. site.oci.tar), else an
|
|
51
|
+
OCI image layout directory.
|
|
52
|
+
Default: the --out extension (.zip .tar .tar.gz .tgz .wbn .oci.tar),
|
|
53
|
+
else the Sitefile's EXPORT ... AS <fmt>
|
|
54
|
+
--oci-root <path> directory inside an oci image layer (default /srv/www; overrides EXPORT ... AT)
|
|
55
|
+
--oci-compression <c> oci site layer: gzip (default) or none (plain tar; overrides EXPORT ... COMPRESSION)
|
|
56
|
+
--label <key=value> add an OCI image config label (repeatable)
|
|
57
|
+
--git-labels add org.opencontainers.image.source/revision from the git checkout
|
|
58
|
+
(off by default: they change the image ID on every commit)
|
|
59
|
+
--provenance attach a SLSA provenance attestation to the oci image (an in-toto
|
|
60
|
+
statement: Sitefile digest, build args, imports, BASE, builder)
|
|
61
|
+
--sbom attach an SBOM attestation (CycloneDX: every file with its SHA-256);
|
|
62
|
+
both are OCI 1.1 referrers of the image (its ID does not change),
|
|
63
|
+
written into the oci export and kept with -t images for push
|
|
64
|
+
--platform <os/arch> oci image platform (default linux/amd64); with a BASE, the platform pulled
|
|
65
|
+
--base <image> build the oci image on <image> (runnable; overrides EXPORT ... AS oci BASE);
|
|
66
|
+
a bare name means Docker Hub. Pin it: nginx:alpine@sha256:...
|
|
67
|
+
--insecure-registry <h> allow plain http to registry host[:port] (repeatable; localhost always may)
|
|
68
|
+
--base-url <url> https URL ending in '/' the files are mounted at (wbn only);
|
|
69
|
+
overrides EXPORT ... BASE
|
|
70
|
+
--build-arg <NAME=value> set an ARG (repeatable); --build-arg NAME reads $NAME
|
|
71
|
+
--secret id=<id>,env=<VAR> | id=<id>,src=<file>
|
|
72
|
+
a build secret for IMPORT ... SECRET <id> AS HEADER <Name> | AS BEARER
|
|
73
|
+
(repeatable); sent only to that import's origin, never recorded
|
|
74
|
+
--plugin-timeout <ms> per-call time limit for TRANSFORM plugins and tools (default 60000)
|
|
75
|
+
--allow-exec <name[=path]> allow a TOOL (external program) to run, resolved on PATH or at
|
|
76
|
+
the absolute path (repeatable); programs run with your privileges
|
|
77
|
+
--allow-plugin <name> allow a JavaScript PLUGIN to run (repeatable; wasm plugins need no flag)
|
|
78
|
+
--allow-plugins allow every JavaScript PLUGIN in the Sitefile
|
|
79
|
+
--no-cache do not read or write the TRANSFORM result cache
|
|
80
|
+
($SITEFILE_HOME/cache/transform; see 'sitefile cache --help')
|
|
81
|
+
--cache-from <ref> also look up TRANSFORM results in this registry cache (build only,
|
|
82
|
+
repeatable, first match wins); results are fetched only on a hit
|
|
83
|
+
--cache-to <ref> after a successful build, push the TRANSFORM results it used to this
|
|
84
|
+
registry cache (build only, once); registry problems only warn
|
|
85
|
+
--build-arg-file <path> read ARGs from a .json, .yaml/.yml or .env file (repeatable;
|
|
86
|
+
later files win, --build-arg wins over files)
|
|
87
|
+
--force replace a non-empty --out outside the context
|
|
88
|
+
--frozen-lockfile fail instead of changing Sitefile.lock
|
|
89
|
+
--update-lock re-record unpinned URLs whose content changed
|
|
90
|
+
--strict-pins fail when a remote IMPORT has no INTEGRITY and no lockfile entry, or the
|
|
91
|
+
BASE image is neither @sha256:-pinned nor locked (build, validate, inspect)
|
|
92
|
+
--lint-disable <rules> turn lint rules off (validate; comma-separated rule ids, or all; repeatable)
|
|
93
|
+
--no-lockfile do not read or write Sitefile.lock
|
|
94
|
+
--vendor-dir <dir> read remote imports and BASE blobs from this vendor directory (relative
|
|
95
|
+
to the context; default vendor/sitefile when it exists; see 'sitefile vendor')
|
|
96
|
+
--allow-http permit plain http:// imports
|
|
97
|
+
--allow-private-network permit loopback/private network addresses
|
|
98
|
+
--allow-hardlinks permit hard-linked files in the context (you assert they are safe)
|
|
99
|
+
--max-download-bytes <n> per-resource size limit (default ${DEFAULT_FETCH_POLICY.maxBytes})
|
|
100
|
+
--timeout <ms> per-resource fetch timeout (default ${DEFAULT_FETCH_POLICY.timeoutMs})
|
|
101
|
+
--json machine-readable output (validate, inspect, graph, why)
|
|
102
|
+
--dot Graphviz DOT output (graph only)
|
|
103
|
+
--watch build, then rebuild when the context, the Sitefile or a
|
|
104
|
+
--build-arg-file changes (build only; Ctrl-C stops)
|
|
105
|
+
--profile <file> write a timing trace of the build (Chrome trace event JSON:
|
|
106
|
+
chrome://tracing, ui.perfetto.dev); --watch rewrites it per build
|
|
107
|
+
-h, --help show this help
|
|
108
|
+
-v, --version show the version
|
|
109
|
+
${EXTRA_HELP}`;
|
|
110
|
+
/**
|
|
111
|
+
* Run the CLI. `run.once` runs a --watch command a single time (the watch loop's
|
|
112
|
+
* builds); `run.wrote` then receives the export destination, and `run.memo` carries
|
|
113
|
+
* results between those builds (incremental rebuilds).
|
|
114
|
+
*/
|
|
115
|
+
export async function main(argv, run = {}) {
|
|
116
|
+
if (isExtraCommand(argv[0]))
|
|
117
|
+
return runExtraCommand(argv);
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = parseArgs({
|
|
121
|
+
args: [...argv],
|
|
122
|
+
allowPositionals: true,
|
|
123
|
+
options: {
|
|
124
|
+
file: { type: 'string', short: 'f' },
|
|
125
|
+
target: { type: 'string' },
|
|
126
|
+
out: { type: 'string', short: 'o' },
|
|
127
|
+
tag: { type: 'string', short: 't', multiple: true },
|
|
128
|
+
format: { type: 'string' },
|
|
129
|
+
'base-url': { type: 'string' },
|
|
130
|
+
'oci-root': { type: 'string' },
|
|
131
|
+
'oci-compression': { type: 'string' },
|
|
132
|
+
label: { type: 'string', multiple: true },
|
|
133
|
+
'git-labels': { type: 'boolean' },
|
|
134
|
+
provenance: { type: 'boolean' },
|
|
135
|
+
sbom: { type: 'boolean' },
|
|
136
|
+
platform: { type: 'string' },
|
|
137
|
+
base: { type: 'string' },
|
|
138
|
+
'insecure-registry': { type: 'string', multiple: true },
|
|
139
|
+
'build-arg': { type: 'string', multiple: true },
|
|
140
|
+
'build-arg-file': { type: 'string', multiple: true },
|
|
141
|
+
secret: { type: 'string', multiple: true },
|
|
142
|
+
'plugin-timeout': { type: 'string' },
|
|
143
|
+
'allow-exec': { type: 'string', multiple: true },
|
|
144
|
+
'allow-plugin': { type: 'string', multiple: true },
|
|
145
|
+
'allow-plugins': { type: 'boolean' },
|
|
146
|
+
'no-cache': { type: 'boolean' },
|
|
147
|
+
'cache-from': { type: 'string', multiple: true },
|
|
148
|
+
'cache-to': { type: 'string', multiple: true },
|
|
149
|
+
force: { type: 'boolean' },
|
|
150
|
+
'frozen-lockfile': { type: 'boolean' },
|
|
151
|
+
'update-lock': { type: 'boolean' },
|
|
152
|
+
'strict-pins': { type: 'boolean' },
|
|
153
|
+
'lint-disable': { type: 'string', multiple: true },
|
|
154
|
+
'no-lockfile': { type: 'boolean' },
|
|
155
|
+
'vendor-dir': { type: 'string' },
|
|
156
|
+
'allow-http': { type: 'boolean' },
|
|
157
|
+
'allow-private-network': { type: 'boolean' },
|
|
158
|
+
'allow-hardlinks': { type: 'boolean' },
|
|
159
|
+
'max-download-bytes': { type: 'string' },
|
|
160
|
+
timeout: { type: 'string' },
|
|
161
|
+
json: { type: 'boolean' },
|
|
162
|
+
dot: { type: 'boolean' },
|
|
163
|
+
watch: { type: 'boolean' },
|
|
164
|
+
profile: { type: 'string' },
|
|
165
|
+
help: { type: 'boolean', short: 'h' },
|
|
166
|
+
version: { type: 'boolean', short: 'v' },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
return usageError(e.message);
|
|
172
|
+
}
|
|
173
|
+
const { values: o, positionals } = parsed;
|
|
174
|
+
if (o.version) {
|
|
175
|
+
process.stdout.write(`${VERSION}\n`);
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
const [command, ...rest] = positionals;
|
|
179
|
+
if (o.help || !command) {
|
|
180
|
+
process.stdout.write(HELP);
|
|
181
|
+
return o.help ? 0 : 2;
|
|
182
|
+
}
|
|
183
|
+
// why <path> [context]
|
|
184
|
+
const whyPath = command === 'why' ? rest.shift() : undefined;
|
|
185
|
+
if (command === 'why' && whyPath === undefined)
|
|
186
|
+
return usageError('why expects a path in the build output: sitefile why <path> [context]');
|
|
187
|
+
const [contextArg = '.', ...extra] = rest;
|
|
188
|
+
if (extra.length)
|
|
189
|
+
return usageError(`unexpected arguments: ${extra.join(' ')}`);
|
|
190
|
+
const gitContext = isGitContext(contextArg);
|
|
191
|
+
if (gitContext && o.watch)
|
|
192
|
+
return usageError('--watch needs a local context directory, not a Git URL');
|
|
193
|
+
if (gitContext && o.file !== undefined && (path.isAbsolute(o.file) || o.file.split(/[\\/]/).includes('..'))) {
|
|
194
|
+
return usageError('with a Git URL context, -f/--file is a path inside the repository');
|
|
195
|
+
}
|
|
196
|
+
if (o.dot && command !== 'graph')
|
|
197
|
+
return usageError('--dot is only for graph');
|
|
198
|
+
if (o.dot && o.json)
|
|
199
|
+
return usageError('--dot and --json conflict');
|
|
200
|
+
if (o['frozen-lockfile'] && o['update-lock'])
|
|
201
|
+
return usageError('--frozen-lockfile and --update-lock conflict');
|
|
202
|
+
if (o['strict-pins'] && o['update-lock'])
|
|
203
|
+
return usageError('--strict-pins and --update-lock conflict');
|
|
204
|
+
const lintDisabled = (o['lint-disable'] ?? []).flatMap((v) => v.split(',').map((s) => s.trim()));
|
|
205
|
+
if (o['lint-disable'] && command !== 'validate')
|
|
206
|
+
return usageError('--lint-disable is only for validate');
|
|
207
|
+
for (const id of lintDisabled) {
|
|
208
|
+
if (id !== 'all' && !LINT_RULES.includes(id)) {
|
|
209
|
+
return usageError(`--lint-disable: unknown lint rule '${id}' (rules: ${LINT_RULES.join(', ')}, or all)`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const cacheFrom = o['cache-from'] ?? [];
|
|
213
|
+
const cacheTo = o['cache-to'] ?? [];
|
|
214
|
+
if (cacheFrom.length || cacheTo.length) {
|
|
215
|
+
if (command !== 'build')
|
|
216
|
+
return usageError('--cache-from and --cache-to are only for build');
|
|
217
|
+
if (o.watch)
|
|
218
|
+
return usageError('--watch cannot be combined with --cache-from or --cache-to');
|
|
219
|
+
if (o['no-cache'])
|
|
220
|
+
return usageError('--no-cache cannot be combined with --cache-from or --cache-to');
|
|
221
|
+
if (cacheTo.length > 1)
|
|
222
|
+
return usageError('--cache-to can be given only once');
|
|
223
|
+
}
|
|
224
|
+
if (o.watch && !run.once) {
|
|
225
|
+
if (command !== 'build')
|
|
226
|
+
return usageError(`--watch is only for build (and serve --watch), not ${command}`);
|
|
227
|
+
if ((o.tag ?? []).length)
|
|
228
|
+
return usageError('--watch cannot be combined with -t');
|
|
229
|
+
try {
|
|
230
|
+
const memo = watchMemo();
|
|
231
|
+
return await watchLoop({
|
|
232
|
+
context: contextArg,
|
|
233
|
+
...(o.file ? { sitefile: o.file } : {}),
|
|
234
|
+
argFiles: o['build-arg-file'] ?? [],
|
|
235
|
+
build: async () => {
|
|
236
|
+
const wrote = [];
|
|
237
|
+
return { code: await main(argv, { once: true, wrote, memo }), wrote };
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
if (!isSitefileError(e))
|
|
243
|
+
throw e;
|
|
244
|
+
process.stderr.write(`error[${e.code}]: ${e.message}\n`);
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (o.profile === '')
|
|
249
|
+
return usageError('--profile expects a file path');
|
|
250
|
+
// --profile: a trace of this command (build, validate, inspect, graph, why), written when it ends.
|
|
251
|
+
const profiler = o.profile !== undefined ? new Profiler(command) : undefined;
|
|
252
|
+
if (o.profile !== undefined)
|
|
253
|
+
run.wrote?.push(o.profile);
|
|
254
|
+
const commandSpan = profiler?.begin(command, 'command');
|
|
255
|
+
let failed = false;
|
|
256
|
+
let fetched;
|
|
257
|
+
try {
|
|
258
|
+
const fetchPolicy = {
|
|
259
|
+
allowHttp: !!o['allow-http'],
|
|
260
|
+
allowPrivateNetwork: !!o['allow-private-network'],
|
|
261
|
+
...(o['max-download-bytes'] ? { maxBytes: positiveInt(o['max-download-bytes'], '--max-download-bytes') } : {}),
|
|
262
|
+
...(o.timeout ? { timeoutMs: positiveInt(o.timeout, '--timeout') } : {}),
|
|
263
|
+
};
|
|
264
|
+
if (gitContext) {
|
|
265
|
+
// A Git URL context: fetched into a temporary directory, built from there, then removed.
|
|
266
|
+
fetched = await materializeGitContext(contextArg, { ...DEFAULT_FETCH_POLICY, ...fetchPolicy });
|
|
267
|
+
process.stderr.write(`context: ${contextArg} at commit ${fetched.commit}\n`);
|
|
268
|
+
}
|
|
269
|
+
const context = fetched?.dir ?? contextArg;
|
|
270
|
+
const file = fetched && o.file !== undefined ? path.join(context, o.file) : o.file;
|
|
271
|
+
const remoteCache = cacheFrom.length || cacheTo.length
|
|
272
|
+
? new RegistryCache({
|
|
273
|
+
from: cacheFrom.map((r) => parseCacheRef('--cache-from', r)),
|
|
274
|
+
to: cacheTo[0] !== undefined ? parseCacheRef('--cache-to', cacheTo[0]) : undefined,
|
|
275
|
+
client: (registry) => makeClient(registry, { insecureRegistries: insecureRegistriesFromEnv(o['insecure-registry'] ?? []), allowPrivateNetwork: !!o['allow-private-network'] }),
|
|
276
|
+
limit: cacheLimitFromEnv(),
|
|
277
|
+
warn: (m) => process.stderr.write(`warning: ${m}\n`),
|
|
278
|
+
notice: (m) => process.stderr.write(`${m}\n`),
|
|
279
|
+
})
|
|
280
|
+
: undefined;
|
|
281
|
+
// Precedence: Sitefile defaults < --build-arg-file (later files win) < --build-arg.
|
|
282
|
+
const buildArgs = await collectBuildArgs(o['build-arg-file'] ?? [], o['build-arg'] ?? []);
|
|
283
|
+
const secrets = await loadSecrets(o.secret ?? []);
|
|
284
|
+
let formatOverride;
|
|
285
|
+
if (o.format !== undefined) {
|
|
286
|
+
formatOverride = parseExportFormat(o.format);
|
|
287
|
+
if (!formatOverride)
|
|
288
|
+
return usageError(`--format expects one of ${EXPORT_FORMATS.join(', ')} (or tar.gz), got '${o.format}'`);
|
|
289
|
+
}
|
|
290
|
+
let ociCompression;
|
|
291
|
+
if (o['oci-compression'] !== undefined) {
|
|
292
|
+
ociCompression = parseOciCompression(o['oci-compression']);
|
|
293
|
+
if (!ociCompression)
|
|
294
|
+
return usageError(`--oci-compression expects gzip or none, got '${o['oci-compression']}'`);
|
|
295
|
+
}
|
|
296
|
+
// No --format: an archive extension on --out picks the format, so `-o site.zip`
|
|
297
|
+
// writes a zip instead of a directory named site.zip.
|
|
298
|
+
if (!formatOverride && o.out)
|
|
299
|
+
formatOverride = formatFromExtension(o.out);
|
|
300
|
+
// The default destination depends on the format, which the Sitefile may set.
|
|
301
|
+
const sitefileFormat = command === 'build' && !o.out && !formatOverride ? (await planBuild({ contextDir: context, ...(file ? { sitefile: file } : {}), buildArgs })).exportFormat : undefined;
|
|
302
|
+
// A Git context's default destination is in the current directory, not the temporary checkout.
|
|
303
|
+
const out = o.out ?? path.join(fetched ? '.' : context, defaultOutName(formatOverride ?? sitefileFormat ?? 'directory'));
|
|
304
|
+
run.wrote?.push(out);
|
|
305
|
+
const options = {
|
|
306
|
+
contextDir: context,
|
|
307
|
+
...(file ? { sitefile: file } : {}),
|
|
308
|
+
...(fetched ? { label: o.file ?? 'Sitefile' } : {}),
|
|
309
|
+
...(o.target !== undefined ? { target: o.target } : {}),
|
|
310
|
+
buildArgs,
|
|
311
|
+
...(secrets.size ? { secrets } : {}),
|
|
312
|
+
outDir: out,
|
|
313
|
+
fetchPolicy,
|
|
314
|
+
...(o['no-lockfile'] ? { lockfile: false } : {}),
|
|
315
|
+
...(o['vendor-dir'] !== undefined ? { vendorDir: o['vendor-dir'] } : {}),
|
|
316
|
+
...(o['plugin-timeout']
|
|
317
|
+
? { pluginPolicy: { timeoutMs: positiveInt(o['plugin-timeout'], '--plugin-timeout') }, toolPolicy: { timeoutMs: positiveInt(o['plugin-timeout'], '--plugin-timeout') } }
|
|
318
|
+
: {}),
|
|
319
|
+
...(o['allow-exec'] ? { allowExec: o['allow-exec'] } : {}),
|
|
320
|
+
...(o['allow-plugin'] ? { allowPlugin: o['allow-plugin'] } : {}),
|
|
321
|
+
allowAllPlugins: !!o['allow-plugins'],
|
|
322
|
+
...(o['no-cache'] ? {} : { cacheDir: transformCacheDir() }),
|
|
323
|
+
...(remoteCache ? { remoteCache } : {}),
|
|
324
|
+
onNotice: (m) => process.stderr.write(`${m}\n`),
|
|
325
|
+
allowHardlinks: !!o['allow-hardlinks'],
|
|
326
|
+
...(o.platform !== undefined ? { platform: o.platform } : {}),
|
|
327
|
+
...(o.base !== undefined ? { baseImage: o.base } : {}),
|
|
328
|
+
// inspect never pulls; a non-oci export (with no image tag) has no use for a BASE
|
|
329
|
+
...(command === 'inspect' || command === 'why' || (command === 'build' && formatOverride !== undefined && formatOverride !== 'oci' && !(o.tag ?? []).length) ? { skipBase: true } : {}),
|
|
330
|
+
registry: { insecureRegistries: insecureRegistriesFromEnv(o['insecure-registry'] ?? []) },
|
|
331
|
+
frozenLockfile: !!o['frozen-lockfile'],
|
|
332
|
+
updateLock: !!o['update-lock'],
|
|
333
|
+
strictPins: !!o['strict-pins'],
|
|
334
|
+
onWarning: (m) => process.stderr.write(`warning: ${m}\n`),
|
|
335
|
+
...(profiler ? { profiler, alsoWrites: [o.profile] } : {}),
|
|
336
|
+
...(run.memo ? { memo: run.memo } : {}),
|
|
337
|
+
};
|
|
338
|
+
switch (command) {
|
|
339
|
+
case 'build': {
|
|
340
|
+
const tags = o.tag ?? [];
|
|
341
|
+
validateImageTags(tags);
|
|
342
|
+
const labels = parseLabels(o.label ?? []);
|
|
343
|
+
const attest = { provenance: !!o.provenance, sbom: !!o.sbom };
|
|
344
|
+
if ((attest.provenance || attest.sbom) && !tags.length && (formatOverride ?? sitefileFormat ?? (await planBuild(options)).exportFormat) !== 'oci') {
|
|
345
|
+
return usageError('--provenance and --sbom attach attestations to an oci image: export one (--format oci, -o *.oci.tar, EXPORT ... AS oci) or store one with -t');
|
|
346
|
+
}
|
|
347
|
+
const imageInfo = {
|
|
348
|
+
contextDir: context,
|
|
349
|
+
ociRoot: o['oci-root'],
|
|
350
|
+
ociCompression,
|
|
351
|
+
platform: o.platform,
|
|
352
|
+
labels,
|
|
353
|
+
gitLabels: !!o['git-labels'],
|
|
354
|
+
attest,
|
|
355
|
+
build: { contextDir: context, ...(file ? { sitefile: file } : {}), ...(fetched ? { label: o.file ?? 'Sitefile' } : {}), buildArgs },
|
|
356
|
+
};
|
|
357
|
+
await remoteCache?.load();
|
|
358
|
+
if (tags.length && !o.out) {
|
|
359
|
+
// -t without -o: store the image only; nothing is written to the context.
|
|
360
|
+
await storeBuildAsImage(await build(options), tags, imageInfo);
|
|
361
|
+
await remoteCache?.push(transformCacheDir());
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
const r = await buildToDirectory({
|
|
365
|
+
...options,
|
|
366
|
+
outDir: out,
|
|
367
|
+
force: !!o.force,
|
|
368
|
+
...(formatOverride ? { format: formatOverride } : {}),
|
|
369
|
+
...(o['base-url'] !== undefined ? { baseUrl: o['base-url'] } : {}),
|
|
370
|
+
...(o['oci-root'] !== undefined ? { ociRoot: o['oci-root'] } : {}),
|
|
371
|
+
...(ociCompression !== undefined ? { ociCompression } : {}),
|
|
372
|
+
...(o.platform !== undefined ? { platform: o.platform } : {}),
|
|
373
|
+
imageLabels: labels,
|
|
374
|
+
gitLabels: !!o['git-labels'],
|
|
375
|
+
...(tags[0] ? { ociRefName: refKey(parseLocalRef(tags[0])) } : {}),
|
|
376
|
+
attest,
|
|
377
|
+
});
|
|
378
|
+
const where = path.relative(process.cwd(), r.outDir) || '.';
|
|
379
|
+
const n = `${r.output.size} file${r.output.size === 1 ? '' : 's'}`;
|
|
380
|
+
process.stdout.write(r.format === 'directory'
|
|
381
|
+
? `Exported ${n} to ${where} (${r.output.digest()})\n`
|
|
382
|
+
: r.format === 'oci'
|
|
383
|
+
? `Exported ${n} to ${where} (oci image ${r.manifestDigest}${r.bytes !== undefined ? `, ${r.bytes} bytes` : ''})\n`
|
|
384
|
+
: `Exported ${n} to ${where} (${r.format === 'tgz' ? 'tar.gz' : r.format}, ${r.bytes} bytes, ${r.fileDigest})\n`);
|
|
385
|
+
// A Git context's lockfile lives in the temporary checkout: it is used, never written back.
|
|
386
|
+
if (r.lockfileWritten && !fetched)
|
|
387
|
+
process.stdout.write('Updated Sitefile.lock\n');
|
|
388
|
+
if (tags.length)
|
|
389
|
+
await storeBuildAsImage(r, tags, imageInfo);
|
|
390
|
+
await remoteCache?.push(transformCacheDir());
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
case 'validate': {
|
|
394
|
+
const span = profiler?.begin('parse', 'build');
|
|
395
|
+
const p = await planBuild(options);
|
|
396
|
+
span?.end({ imports: p.imports.length, stages: p.stages.length, ...(p.target !== undefined ? { target: p.target } : {}) });
|
|
397
|
+
p.warnings.forEach((m) => process.stderr.write(`warning: ${m}\n`));
|
|
398
|
+
(await lintBuild(options, lintDisabled)).forEach((m) => process.stderr.write(`warning: ${m}\n`));
|
|
399
|
+
if (o['strict-pins'])
|
|
400
|
+
await checkPlanPins(options, p);
|
|
401
|
+
if (o.json) {
|
|
402
|
+
process.stdout.write(JSON.stringify({ file: p.file, vars: Object.fromEntries(p.vars), imports: p.imports, stages: p.stages, export: p.exportPath, ...(p.target !== undefined ? { target: p.target } : {}) }, null, 2) + '\n');
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
const opCount = p.stages.reduce((n, st) => n + st.operations.length, 0);
|
|
406
|
+
process.stdout.write(`${p.file}: OK (${p.imports.length} import${p.imports.length === 1 ? '' : 's'}, ${opCount} operation${opCount === 1 ? '' : 's'}${p.stages.length > 1 ? `, ${p.stages.length} stages` : ''}${p.target !== undefined ? `, target ${p.target}` : ''})\n`);
|
|
407
|
+
}
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
case 'inspect': {
|
|
411
|
+
const r = await build({ ...options, frozenLockfile: true });
|
|
412
|
+
const files = r.output.entries().map(([p, n]) => ({ path: p, mode: n.mode.toString(8), size: n.bytes.byteLength, digest: n.digest, origin: n.origin }));
|
|
413
|
+
if (o.json) {
|
|
414
|
+
process.stdout.write(JSON.stringify({ digest: r.output.digest(), files }, null, 2) + '\n');
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
for (const f of files)
|
|
418
|
+
process.stdout.write(`${f.mode} ${String(f.size).padStart(8)} ${f.path} (${f.origin})\n`);
|
|
419
|
+
process.stdout.write(`${files.length} files, ${r.output.digest()}\n`);
|
|
420
|
+
}
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
case 'graph': {
|
|
424
|
+
const span = profiler?.begin('parse', 'build');
|
|
425
|
+
const p = await planBuild(options);
|
|
426
|
+
span?.end({ imports: p.imports.length, stages: p.stages.length, ...(p.target !== undefined ? { target: p.target } : {}) });
|
|
427
|
+
p.warnings.forEach((m) => process.stderr.write(`warning: ${m}\n`));
|
|
428
|
+
if (o['strict-pins'])
|
|
429
|
+
await checkPlanPins(options, p);
|
|
430
|
+
const g = planGraph(p);
|
|
431
|
+
process.stdout.write(o.json ? JSON.stringify(g, null, 2) + '\n' : o.dot ? graphDot(g) : graphText(g));
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
case 'why': {
|
|
435
|
+
const r = await build({ ...options, frozenLockfile: true, recordSteps: true });
|
|
436
|
+
const report = why(r, whyPath, context);
|
|
437
|
+
process.stdout.write(o.json ? JSON.stringify(report, null, 2) + '\n' : whyText(report));
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
default:
|
|
441
|
+
return usageError(`unknown command '${command}'`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
failed = true;
|
|
446
|
+
if (isSitefileError(e)) {
|
|
447
|
+
if (e.code === 'E_USAGE')
|
|
448
|
+
return usageError(e.detail);
|
|
449
|
+
process.stderr.write(`error[${e.code}]: ${e.message}\n`);
|
|
450
|
+
return 1;
|
|
451
|
+
}
|
|
452
|
+
throw e;
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
// Keep $SITEFILE_HOME under $SITEFILE_CACHE_MAX (never fails the command).
|
|
456
|
+
if (command === 'build' || command === 'inspect' || command === 'why')
|
|
457
|
+
await evictAfterBuild();
|
|
458
|
+
await fetched?.dispose();
|
|
459
|
+
commandSpan?.end();
|
|
460
|
+
if (profiler && !(await fs.writeFile(o.profile, profiler.toJSON()).then(() => true, () => false))) {
|
|
461
|
+
// A failed build already reported its error; otherwise this is the command's error.
|
|
462
|
+
if (!failed) {
|
|
463
|
+
process.stderr.write(`error[E_EXPORT]: cannot write the --profile file '${o.profile}'\n`);
|
|
464
|
+
// eslint-disable-next-line no-unsafe-finally
|
|
465
|
+
return 1;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function positiveInt(raw, flag) {
|
|
471
|
+
const n = Number(raw);
|
|
472
|
+
if (!Number.isSafeInteger(n) || n <= 0)
|
|
473
|
+
throw new SitefileError('E_USAGE', `${flag} expects a positive integer`);
|
|
474
|
+
return n;
|
|
475
|
+
}
|
|
476
|
+
function usageError(message) {
|
|
477
|
+
process.stderr.write(`error: ${message}\nRun 'sitefile --help' for usage.\n`);
|
|
478
|
+
return 2;
|
|
479
|
+
}
|
|
480
|
+
main(process.argv.slice(2)).then((code) => {
|
|
481
|
+
process.exitCode = code;
|
|
482
|
+
}, (e) => {
|
|
483
|
+
process.stderr.write(`internal error: ${e?.stack ?? String(e)}\n`);
|
|
484
|
+
process.exitCode = 70;
|
|
485
|
+
});
|
|
486
|
+
//# sourceMappingURL=cli.js.map
|
package/lib/src/csv.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 4180 CSV parser (no dependencies).
|
|
3
|
+
*
|
|
4
|
+
* - Fields may be double-quoted; inside quotes `""` is a literal quote and
|
|
5
|
+
* delimiters / newlines are data. Records end at LF or CRLF; a trailing
|
|
6
|
+
* newline is optional; a leading UTF-8 BOM is stripped.
|
|
7
|
+
* - Completely empty lines are skipped.
|
|
8
|
+
* - A quote inside an unquoted field, text after a closing quote, or an
|
|
9
|
+
* unterminated quote is E_DATA with line and column.
|
|
10
|
+
* - `header: true` (default): the first record names the columns (non-empty,
|
|
11
|
+
* unique) and every record must have the same number of fields. Values are
|
|
12
|
+
* always strings. With `header: false`, records are returned as string
|
|
13
|
+
* arrays and may have differing lengths.
|
|
14
|
+
* - Rows are ordinary objects; a `__proto__` column is stored as plain data.
|
|
15
|
+
*/
|
|
16
|
+
export interface CsvOptions {
|
|
17
|
+
header?: boolean;
|
|
18
|
+
delimiter?: ',' | ';' | '\t';
|
|
19
|
+
}
|
|
20
|
+
export declare function parseCsv(text: string, file: string, options?: CsvOptions & {
|
|
21
|
+
header?: true;
|
|
22
|
+
}): Array<Record<string, string>>;
|
|
23
|
+
export declare function parseCsv(text: string, file: string, options: CsvOptions & {
|
|
24
|
+
header: false;
|
|
25
|
+
}): string[][];
|
|
26
|
+
export declare function parseCsv(text: string, file: string, options?: CsvOptions): Array<Record<string, string>> | string[][];
|
|
27
|
+
export declare function parseCsvRecords(text: string, file: string): Array<Record<string, string>>;
|
package/lib/src/csv.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { SitefileError } from './errors.js';
|
|
2
|
+
export function parseCsv(text, file, options = {}) {
|
|
3
|
+
const header = options.header ?? true;
|
|
4
|
+
const delim = options.delimiter ?? ',';
|
|
5
|
+
const rows = tokenize(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text, file, delim);
|
|
6
|
+
if (!header)
|
|
7
|
+
return rows.map((r) => r.fields);
|
|
8
|
+
const head = rows[0];
|
|
9
|
+
if (!head)
|
|
10
|
+
return [];
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
head.fields.forEach((name, i) => {
|
|
13
|
+
const span = { file, line: head.line, column: head.cols[i] };
|
|
14
|
+
if (name === '')
|
|
15
|
+
throw new SitefileError('E_DATA', `empty CSV column name in header (column ${i + 1})`, span);
|
|
16
|
+
if (seen.has(name))
|
|
17
|
+
throw new SitefileError('E_DATA', `duplicate CSV column name '${name}'`, span);
|
|
18
|
+
seen.add(name);
|
|
19
|
+
});
|
|
20
|
+
return rows.slice(1).map((row) => {
|
|
21
|
+
if (row.fields.length !== head.fields.length) {
|
|
22
|
+
throw new SitefileError('E_DATA', `expected ${head.fields.length} field${head.fields.length === 1 ? '' : 's'} but found ${row.fields.length}`, { file, line: row.line, column: 1 });
|
|
23
|
+
}
|
|
24
|
+
// fromEntries defines own data properties, so `__proto__` is safe.
|
|
25
|
+
return Object.fromEntries(head.fields.map((name, i) => [name, row.fields[i]]));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function parseCsvRecords(text, file) {
|
|
29
|
+
return parseCsv(text, file, { header: true });
|
|
30
|
+
}
|
|
31
|
+
function tokenize(text, file, delim) {
|
|
32
|
+
const n = text.length;
|
|
33
|
+
const rows = [];
|
|
34
|
+
let i = 0;
|
|
35
|
+
let line = 1;
|
|
36
|
+
let lineStart = 0;
|
|
37
|
+
const fail = (detail, l, index, ls) => {
|
|
38
|
+
throw new SitefileError('E_DATA', detail, { file, line: l, column: index - ls + 1 });
|
|
39
|
+
};
|
|
40
|
+
while (i < n) {
|
|
41
|
+
// Skip fully empty lines.
|
|
42
|
+
if (text[i] === '\n') {
|
|
43
|
+
i++;
|
|
44
|
+
line++;
|
|
45
|
+
lineStart = i;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (text[i] === '\r' && text[i + 1] === '\n') {
|
|
49
|
+
i += 2;
|
|
50
|
+
line++;
|
|
51
|
+
lineStart = i;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const row = { fields: [], cols: [], line };
|
|
55
|
+
for (;;) {
|
|
56
|
+
row.cols.push(i - lineStart + 1);
|
|
57
|
+
let field = '';
|
|
58
|
+
if (text[i] === '"') {
|
|
59
|
+
const qLine = line;
|
|
60
|
+
const qIndex = i;
|
|
61
|
+
const qStart = lineStart;
|
|
62
|
+
i++;
|
|
63
|
+
for (;;) {
|
|
64
|
+
if (i >= n)
|
|
65
|
+
return fail('unterminated quoted field', qLine, qIndex, qStart);
|
|
66
|
+
const c = text[i];
|
|
67
|
+
if (c === '"') {
|
|
68
|
+
if (text[i + 1] === '"') {
|
|
69
|
+
field += '"';
|
|
70
|
+
i += 2;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
i++;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (c === '\n') {
|
|
77
|
+
line++;
|
|
78
|
+
lineStart = i + 1;
|
|
79
|
+
}
|
|
80
|
+
field += c;
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
const after = text[i];
|
|
84
|
+
const atEnd = i >= n || after === delim || after === '\n' || (after === '\r' && text[i + 1] === '\n');
|
|
85
|
+
if (!atEnd)
|
|
86
|
+
return fail('unexpected character after closing quote', line, i, lineStart);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const s = i;
|
|
90
|
+
while (i < n) {
|
|
91
|
+
const c = text[i];
|
|
92
|
+
if (c === delim || c === '\n' || (c === '\r' && text[i + 1] === '\n'))
|
|
93
|
+
break;
|
|
94
|
+
if (c === '"')
|
|
95
|
+
return fail('quote inside an unquoted field', line, i, lineStart);
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
field = text.slice(s, i);
|
|
99
|
+
}
|
|
100
|
+
row.fields.push(field);
|
|
101
|
+
if (i >= n)
|
|
102
|
+
break;
|
|
103
|
+
if (text[i] === delim) {
|
|
104
|
+
i++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Record terminator.
|
|
108
|
+
i += text[i] === '\r' ? 2 : 1;
|
|
109
|
+
line++;
|
|
110
|
+
lineStart = i;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
rows.push(row);
|
|
114
|
+
}
|
|
115
|
+
return rows;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=csv.js.map
|